LLMS_Order

LLMS_Order model.


Description Description


Source Source

File: includes/models/model.llms.order.php

class LLMS_Order extends LLMS_Post_Model {

	protected $db_post_type = 'llms_order';
	protected $model_post_type = 'order';

	protected $properties = array(

		'anonymized' => 'yesno',
		'coupon_amount' => 'float',
		'coupon_amout_trial' => 'float',
		'coupon_value' => 'float',
		'coupon_value_trial' => 'float',
		'original_total' => 'float',
		'sale_price' => 'float',
		'sale_value' => 'float',
		'total' => 'float',
		'trial_original_total' => 'float',
		'trial_total' => 'float',

		'access_length' => 'absint',
		'billing_frequency' => 'absint',
		'billing_length' => 'absint',
		'coupon_id' => 'absint',
		'plan_id' => 'absint',
		'product_id' => 'absint',
		'trial_length' => 'absint',
		'user_id' => 'absint',

		'access_expiration' => 'text',
		'access_expires' => 'text',
		'access_period' => 'text',
		'billing_address_1' => 'text',
		'billing_address_2' => 'text',
		'billing_city' => 'text',
		'billing_country' => 'text',
		'billing_email' => 'text',
		'billing_first_name' => 'text',
		'billing_last_name' => 'text',
		'billing_state' => 'text',
		'billing_zip' => 'text',
		'billing_period' => 'text',
		'coupon_code' => 'text',
		'coupon_type' => 'text',
		'coupon_used' => 'text',
		'currency' => 'text',
		'on_sale' => 'text',
		'order_key' => 'text',
		'order_type' => 'text',
		'payment_gateway' => 'text',
		'plan_sku' => 'text',
		'plan_title' => 'text',
		'product_sku' => 'text',
		'product_type' => 'text',
		'title' => 'text',
		'gateway_api_mode' => 'text',
		'gateway_customer_id' => 'text',
		'trial_offer' => 'text',
		'trial_period' => 'text',
		'user_ip_address' => 'text',

		'date_access_expires' => 'text',
		'date_billing_end' => 'text',
		'date_next_payment' => 'text',
		'date_trial_end' => 'text',

	);

	/**
	 * Add an admin-only note to the order visible on the admin panel
	 * notes are recorded using the wp comments API & DB
	 *
	 * @param    string     $note           note content
	 * @param    boolean    $added_by_user  if this is an admin-submitted note adds user info to note meta
	 * @return   null|int                   null on error or WP_Comment ID of the note
	 * @since    3.0.0
	 * @version  3.24.0
	 */
	public function add_note( $note, $added_by_user = false ) {

		if ( ! $note ) {
			return;
		}

		// added by a user from the admin panel
		if ( $added_by_user && is_user_logged_in() && current_user_can( apply_filters( 'lifterlms_admin_order_access', 'manage_options' ) ) ) {

			$user_id = get_current_user_id();
			$user = get_user_by( 'id', $user_id );
			$author = $user->display_name;
			$author_email = $user->user_email;

		} else {

			$user_id = 0;
			$author = _x( 'LifterLMS', 'default order note author', 'lifterlms' );
			$author_email = strtolower( _x( 'LifterLms', 'default order note author', 'lifterlms' ) ) . '@';
			$author_email .= isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', $_SERVER['HTTP_HOST'] ) : 'noreply.com';
			$author_email = sanitize_email( $author_email );

		}

		$note_id = wp_insert_comment( apply_filters( 'llms_add_order_note_content', array(
			'comment_post_ID' => $this->get( 'id' ),
			'comment_author' => $author,
			'comment_author_email' => $author_email,
			'comment_author_url' => '',
			'comment_content' => $note,
			'comment_type' => 'llms_order_note',
			'comment_parent' => 0,
			'user_id' => $user_id,
			'comment_approved' => 1,
			'comment_agent' => 'LifterLMS',
			'comment_date' => current_time( 'mysql' ),
		) ) );

		do_action( 'llms_new_order_note_added', $note_id, $this );

		return $note_id;

	}

	/**
	 * Called after inserting a new order into the database
	 * @return  void
	 * @since   3.0.0
	 * @version 3.0.0
	 */
	protected function after_create() {
		// add a random key that can be passed in the URL and whatever
		$this->set( 'order_key', $this->generate_order_key() );
	}

	/**
	 * Calculate the date when billing should
	 * applicable to orders created from plans with a set # of billing intervals
	 * @return   int
	 * @since    3.10.0
	 * @version  3.10.0
	 */
	private function calculate_billing_end_date() {

		$end = 0;

		$num_payments = $this->get( 'billing_length' );
		if ( $num_payments ) {

			$start = $this->get_date( 'date', 'U' );

			$period = $this->get( 'billing_period' );
			$frequency = $this->get( 'billing_frequency' );

			$end = $start;

			$i = 0;
			while ( $i < $num_payments ) {
				$end = strtotime( '+' . $frequency . ' ' . $period, $end );
				$i++;
			}
		}

		return apply_filters( 'llms_order_calculate_billing_end_date', $end, $this );

	}

	/**
	 * Calculate the next payment due date
	 * @param    string     $format  return format
	 * @return   string
	 * @since    3.10.0
	 * @version  3.12.0
	 */
	private function calculate_next_payment_date( $format = 'Y-m-d H:i:s' ) {

		$start_time = $this->get_date( 'date', 'U' );
		$end_time = $this->get_date( 'date_billing_end', 'U' );
		if ( ! $end_time && $this->get( 'billing_length' ) ) {
			$end_time = $this->calculate_billing_end_date();
			$this->set( 'date_billing_end', date_i18n( 'Y-m-d H:i:s', $end_time ) );
		}
		$next_payment_time = $this->get_date( 'date_next_payment', 'U' );

		// if were on a trial and the trial hasn't ended yet next payment date is the date the trial ends
		if ( $this->has_trial() && ! $this->has_trial_ended() ) {

			$next_payment_time = $this->get_trial_end_date( 'U' );

		} else {

			// assume we'll start from the order start date
			$from_time = $start_time;

			if ( $next_payment_time && $next_payment_time < llms_current_time( 'timestamp' ) ) {
				// if we have a saved next payment that's old we can calculate from there

				$from_time = $next_payment_time;

			} else {

				// check previous transactions and get the date from there
				// this will be true of orders created prior to 3.10 when no payment dates were saved
				$last_txn = $this->get_last_transaction( array( 'llms-txn-succeeded', 'llms-txn-refunded' ), 'recurring' );
				$last_txn_time = $last_txn ? $last_txn->get_date( 'date', 'U' ) : 0;
				if ( $last_txn_time && $last_txn_time < llms_current_time( 'timestamp' ) ) {
					$from_time = $last_txn_time;
				}
			}

			$period = $this->get( 'billing_period' );
			$frequency = $this->get( 'billing_frequency' );
			$next_payment_time = strtotime( '+' . $frequency . ' ' . $period, $from_time );

			// Make sure the next payment is more than 2 hours in the future
			// this ensures changes to the site's timezone because of daylight savings will never cause a 2nd renewal payment to be processed on the same day
			// thanks WooCommerce Subscriptions <3
			$i = 1;
			while ( $next_payment_time < ( llms_current_time( 'timestamp', true ) + 2 * HOUR_IN_SECONDS ) && $i < 3000 ) {
				$next_payment_time = strtotime( '+' . $frequency . ' ' . $period, $next_payment_time );
				$i++;
			}
		}// End if().

		// if the next payment is after the end time (where applicable)
		if ( 0 != $end_time && ( $next_payment_time + 23 * HOUR_IN_SECONDS ) > $end_time ) {
			$ret = '';
		} elseif ( $next_payment_time > 0 ) {
			$ret = date_i18n( $format, $next_payment_time );
		}

		return apply_filters( 'llms_order_calculate_next_payment_date', $ret, $format, $this );

	}

	/**
	 * Calculate the end date of the trial
	 * @param    string     $format  desired return format of the date
	 * @return   string
	 * @since    3.10.0
	 * @version  3.10.0
	 */
	private function calculate_trial_end_date( $format = 'Y-m-d H:i:s' ) {

		$start = $this->get_date( 'date', 'U' ); // start with the date the order was initially created

		$length = $this->get( 'trial_length' );
		$period = $this->get( 'trial_period' );

		$end = strtotime( '+' . $length . ' ' . $period, $start );

		$ret = date_i18n( $format, $end );

		return apply_filters( 'llms_order_calculate_trial_end_date', $ret, $format, $this );

	}

	/**
	 * Determine if the order can be retried for recurring payments
	 * @return   boolean
	 * @since    3.10.0
	 * @version  3.10.0
	 */
	public function can_be_retried() {

		// only recurring orders can be retried
		if ( ! $this->is_recurring() ) {
			return false;
		}

		if ( 'yes' !== get_option( 'lifterlms_recurring_payment_retry', 'yes' ) ) {
			return false;
		}

		// only active & on-hold orders qualify for a retry
		if ( ! in_array( $this->get( 'status' ), array( 'llms-active', 'llms-on-hold' ) ) ) {
			return false;
		}

		// if the gateway isn't active or the gateway doesn't support recurring retries
		$gateway = $this->get_gateway();
		if ( is_wp_error( $gateway ) || ! $gateway->supports( 'recurring_retry' ) ) {
			return false;
		}

		// if we're here, we can retry
		return true;

	}

	/**
	 * Determine if an order can be resubscribed to
	 * @return   bool
	 * @since    3.19.0
	 * @version  3.19.0
	 */
	public function can_resubscribe() {

		$ret = false;

		if ( $this->is_recurring() ) {

			$allowed_statuses = apply_filters( 'llms_order_status_can_resubscribe_from', array(
				'llms-on-hold',
				'llms-pending',
				'llms-pending-cancel',
			) );
			$ret = in_array( $this->get( 'status' ), $allowed_statuses );

		}

		return apply_filters( 'llms_order_can_resubscribe', $ret, $this );

	}

	/**
	 * Generate an order key for the order
	 * @return   string
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function generate_order_key() {
		return apply_filters( 'lifterlms_generate_order_key', uniqid( 'order-' ) );
	}

	/**
	 * Determine the date when access will expire
	 * based on the access settings of the access plan
	 * at the $start_date of access
	 *
	 * @param    string     $format  date format
	 * @return   string              date string
	 *                               "Lifetime Access" for plans with lifetime access
	 *                               "To be Determined" for limited date when access hasn't started yet
	 * @since    3.0.0
	 * @version  3.19.0
	 */
	public function get_access_expiration_date( $format = 'Y-m-d' ) {

		$type = $this->get( 'access_expiration' );

		$ret = $this->get_date( 'date_access_expires', $format );
		if ( ! $ret ) {

			switch ( $type ) {
				case 'lifetime':
					$ret = __( 'Lifetime Access', 'lifterlms' );
				break;

				case 'limited-date':
					$ret = date_i18n( $format, ( $this->get_date( 'access_expires', 'U' ) + ( DAY_IN_SECONDS - 1 ) ) );
				break;

				case 'limited-period':
					if ( $this->get( 'start_date' ) ) {
						$time = strtotime( '+' . $this->get( 'access_length' ) . ' ' . $this->get( 'access_period' ), $this->get_date( 'start_date', 'U' ) ) + ( DAY_IN_SECONDS - 1 );
						$ret = date_i18n( $format, $time );
					} else {
						$ret = __( 'To be Determined', 'lifterlms' );
					}
				break;

				default:
					$ret = apply_filters( 'llms_order_' . $type . '_access_expiration_date', $type, $this, $format );

			}
		}

		return apply_filters( 'llms_order_get_access_expiration_date', $ret, $this, $format );

	}

	/**
	 * Get the current status of a student's access based on the access plan data
	 * stored on the order at the time of purchase
	 * @return   string        'inactive' if the order is refunded, failed, pending, etc...
	 *                         'expired'  if access has expired according to $this->get_access_expiration_date()
	 *                         'active'   otherwise
	 * @since    3.0.0
	 * @version  3.19.0
	 */
	public function get_access_status() {

		$statuses = apply_filters( 'llms_order_allow_access_stasuses', array(
			'llms-active',
			'llms-completed',
			'llms-pending-cancel',
			// recurring orders can expire but still grant access
			// eg: 3monthly payments grants 1 year of access
			// on the 4th month the order will be marked as expired
			// but the access has not yet expired based on the data below
			'llms-expired',
		) );

		// if the order doesn't have one of the allowed statuses
		// return 'inactive' and don't bother checking expiration data
		if ( ! in_array( $this->get( 'status' ), $statuses ) ) {

			return 'inactive';

		}

		// get the expiration date as a timestamp
		$expires = $this->get_access_expiration_date( 'U' );

		// a translated non-numeric string will be returned for lifetime access
		// so if we have a timestamp we should compare it against the current time
		// to determine if access has expired
		if ( is_numeric( $expires ) ) {

			$now = llms_current_time( 'timestamp' );

			// expiration date is in the past
			// eg: the access has already expired
			if ( $expires < $now ) {

				return 'expired';

			}
		}

		// we're active
		return 'active';

	}

	/**
	 * Retrieve arguments passed to order-related events processed by the action scheduler
	 * @return   array
	 * @since    3.19.0
	 * @version  3.19.0
	 */
	protected function get_action_args() {
		return array(
			'order_id' => $this->get( 'id' ),
		);
	}

	/**
	 * Get the formatted coupon amount with a currency symbol or percentage
	 * @param    string     $payment  coupon discount type, either 'regular' or 'trial'
	 * @return   string
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function get_coupon_amount( $payment = 'regular' ) {

		if ( 'regular' === $payment ) {
			$amount = $this->get( 'coupon_amount' );
		} elseif ( 'trial' === $payment ) {
			$amount = $this->get( 'coupon_amount_trial' );
		}

		$type = $this->get( 'coupon_type' );
		if ( 'percent' === $type ) {
			$amount = $amount . '%';
		} elseif ( 'dollar' === $type ) {
			$amount = llms_price( $amount );
		}
		return $amount;

	}

	/**
	 * Retrieve the customer's full name
	 * @return   string
	 * @since    3.0.0
	 * @version  3.18.0
	 */
	public function get_customer_name() {
		if ( 'yes' === $this->get( 'anonymized' ) ) {
			return __( 'Anonymous', 'lifterlms' );
		}
		return trim( $this->get( 'billing_first_name' ) . ' ' . $this->get( 'billing_last_name' ) );
	}

	/**
	 * An array of default arguments to pass to $this->create()
	 * when creating a new post
	 * @param    string  $title   Title to create the post with
	 * @return   array
	 * @since    3.0.0
	 * @version  3.10.0
	 */
	protected function get_creation_args( $title = '' ) {

		if ( empty( $title ) ) {
			$title = sprintf( __( 'Order &ndash; %s', 'lifterlms' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'lifterlms' ), current_time( 'timestamp' ) ) );
		}

		return apply_filters( 'llms_' . $this->model_post_type . '_get_creation_args', array(
			'comment_status' => 'closed',
			'ping_status'	 => 'closed',
			'post_author' 	 => 1,
			'post_content'   => '',
			'post_excerpt'   => '',
			'post_password'	 => uniqid( 'order_' ),
			'post_status' 	 => 'llms-' . apply_filters( 'llms_default_order_status', 'pending' ),
			'post_title'     => $title,
			'post_type' 	 => $this->get( 'db_post_type' ),
		), $this );
	}

	/**
	 * Retrieve the payment gateway instance for the order's selected payment gateway
	 * @return   instance of an LLMS_Gateway
	 * @since    1.0.0
	 * @version  1.0.0
	 */
	public function get_gateway() {
		$gateways = LLMS()->payment_gateways();
		$gateway = $gateways->get_gateway_by_id( $this->get( 'payment_gateway' ) );
		if ( $gateway && ( $gateway->is_enabled() || is_admin() ) ) {
			return $gateway;
		} else {
			return new WP_Error( 'error', sprintf( __( 'Payment gateway %s could not be located or is no longer enabled', 'lifterlms' ), $this->get( 'payment_gateway' ) ) );
		}
	}

	/**
	 * Get the initial payment amount due on checkout
	 * This will always be the value of "total" except when the product has a trial
	 * @return   mixed
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function get_initial_price( $price_args = array(), $format = 'html' ) {

		if ( $this->has_trial() ) {
			$price = 'trial_total';
		} else {
			$price = 'total';
		}

		return $this->get_price( $price, $price_args, $format );
	}


	/**
	 * Get an array of the order notes
	 * Each note is actually a WordPress comment
	 * @param    integer    $number  number of comments to return
	 * @param    integer    $page    page number for pagination
	 * @return   array
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function get_notes( $number = 10, $page = 1 ) {

		$comments = get_comments( array(
			'status' => 'approve',
			'number'  => $number,
			'offset'  => ( $page - 1 ) * $number,
			'post_id' => $this->get( 'id' ),
		) );

		return $comments;

	}

	/**
	 * Retrieve an LLMS_Post_Model object for the associated product
	 * @return   obj       LLMS_Course / LLMS_Membership instance
	 * @since    3.8.0
	 * @version  3.8.0
	 */
	public function get_product() {
		return llms_get_post( $this->get( 'product_id' ) );
	}

	/**
	 * Retrieve the last (most recent) transaction processed for the order
	 * @param    array|string  $status  filter by status (see transaction statuses)
	 * @param    array|string  $type    filter by type [recurring|single|trial]
	 * @return   obj|false              instance of the LLMS_Transaction or false if none found
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function get_last_transaction( $status = 'any', $type = 'any' ) {
		$txns = $this->get_transactions( array(
			'per_page' => 1,
			'status' => $status,
			'type' => $type,
		) );
		if ( $txns['count'] ) {
			return array_pop( $txns['transactions'] );
		}
		return false;
	}

	/**
	 * Retrieve the date of the last (most recent) transaction
	 * @param    array|string  $status  filter by status (see transaction statuses)
	 * @param    array|string  $type    filter by type [recurring|single|trial]
	 * @param    string        $format  date format of the return
	 * @return   string|false           date or false if none found
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function get_last_transaction_date( $status = 'llms-txn-succeeded', $type = 'any', $format = 'Y-m-d H:i:s' ) {
		$txn = $this->get_last_transaction( $status, $type );
		if ( $txn ) {
			return $txn->get_date( 'date', $format );
		} else {
			return false;
		}
	}

	/**
	 * Retrieve the due date of the next payment according to access plan terms
	 * @param    string     $format  date format to return the date in (see php date())
	 * @return   string
	 * @since    3.0.0
	 * @version  3.19.0
	 */
	public function get_next_payment_due_date( $format = 'Y-m-d H:i:s' ) {

		// single payments will never have a next payment date
		if ( ! $this->is_recurring() ) {
			return new WP_Error( 'not-recurring', __( 'Order is not recurring', 'lifterlms' ) );
		} elseif ( ! in_array( $this->get( 'status' ), array( 'llms-active', 'llms-failed', 'llms-on-hold', 'llms-pending', 'llms-pending-cancel' ) ) ) {
			return new WP_Error( 'invalid-status', __( 'Invalid order status', 'lifterlms' ), $this->get( 'status' ) );
		}

		// retrieve the saved due date
		$next_payment_date = $this->get_date( 'date_next_payment', 'U' );

		// calculate it if not saved
		if ( ! $next_payment_date ) {
			$next_payment_date = $this->calculate_next_payment_date( 'U' );
			if ( ! $next_payment_date ) {
				return new WP_Error( 'plan-ended', __( 'No more payments due', 'lifterlms' ) );
			}
		}

		return date_i18n( $format, apply_filters( 'llms_order_get_next_payment_due_date', $next_payment_date, $this, $format ) );

	}

	/**
	 * Get configured payment retry rules
	 * @return   array
	 * @since    3.10.0
	 * @version  3.10.0
	 */
	private function get_retry_rules() {

		$rules = array(
			array(
				'delay' => HOUR_IN_SECONDS * 12,
				'status' => 'on-hold',
				'notifications' => false,
			),
			array(
				'delay' => DAY_IN_SECONDS,
				'status' => 'on-hold',
				'notifications' => true,
			),
			array(
				'delay' => DAY_IN_SECONDS * 2,
				'status' => 'on-hold',
				'notifications' => true,
			),
			array(
				'delay' => DAY_IN_SECONDS * 3,
				'status' => 'on-hold',
				'notifications' => true,
			),
		);

		return apply_filters( 'llms_order_automatic_retry_rules', $rules, $this );

	}

	/**
	 * SQL query to retrieve total amounts for transactions by type
	 * @param    stirng  $type  'amount' or 'refund_amount'
	 * @return   float
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function get_transaction_total( $type = 'amount' ) {

		$statuses = array( 'llms-txn-refunded' );

		if ( 'amount' === $type ) {
			$statuses[] = 'llms-txn-succeeded';
		}

		$post_statuses = '';
		foreach ( $statuses as $i => $status ) {
			$post_statuses .= " p.post_status = '$status'";
			if ( $i + 1 < count( $statuses ) ) {
				$post_statuses .= 'OR';
			}
		}

		global $wpdb;
		$grosse = $wpdb->get_var( $wpdb->prepare(
			"SELECT SUM( m2.meta_value )
			 FROM $wpdb->posts AS p
			 LEFT JOIN $wpdb->postmeta AS m1 ON m1.post_id = p.ID -- join for the ID
			 LEFT JOIN $wpdb->postmeta AS m2 ON m2.post_id = p.ID -- get the actual amounts
			 WHERE p.post_type = 'llms_transaction'
			   AND ( $post_statuses )
			   AND m1.meta_key = '{$this->meta_prefix}order_id'
			   AND m1.meta_value = %d
			   AND m2.meta_key = '{$this->meta_prefix}{$type}'
			;"
		, array( $this->get( 'id' ) ) ) );

		return floatval( $grosse );
	}

	/**
	 * Get the start date for the order
	 * gets the date of the first initially successful transaction
	 * if none found, uses the created date of the order
	 * @param    string     $format  desired return format of the date
	 * @return   string
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function get_start_date( $format = 'Y-m-d H:i:s' ) {
		// get the first recorded transaction
		// refunds are okay b/c that would have initially given the user access
		$txns = $this->get_transactions( array(
			'order' => 'ASC',
			'orderby' => 'date',
			'per_page' => 1,
			'status' => array( 'llms-txn-succeeded', 'llms-txn-refunded' ),
			'type' => 'any',
		) );
		if ( $txns['count'] ) {
			$txn = array_pop( $txns['transactions'] );
			$date = $txn->get_date( 'date', $format );
		} else {
			$date = $this->get_date( 'date', $format );
		}
		return apply_filters( 'llms_order_get_start_date', $date, $this );
	}

	/**
	 * Retrieve an array of transactions associated with the order according to supplied arguments
	 * @param    array      $args  array of query argument data, see example of arguments below
	 * @return   array
	 * @since    3.0.0
	 * @version  3.10.0
	 */
	public function get_transactions( $args = array() ) {

		extract( wp_parse_args( $args, array(
			'status' => 'any', // string or array or post statuses
			'type' => 'any',   // string or array of transaction types [recurring|single|trial]
			'per_page' => 50,  // int, number of transactions to return
			'paged' => 1,      // int, page number of transactions to return
			'order' => 'DESC',    //
			'orderby' => 'date',  // field to order results by
		) ) );

		// assume any and use this to check for valid statuses
		$statuses = llms_get_transaction_statuses();

		// check statuses
		if ( 'any' !== $statuses ) {

			// if status is a string, ensure it's a valid status
			if ( is_string( $status ) && in_array( $status, $statuses ) ) {
				$statuses = array( $status );
			} elseif ( is_array( $status ) ) {
				$temp = array();
				foreach ( $status as $stat ) {
					if ( in_array( $stat, $statuses ) ) {
						$temp[] = $stat;
					}
				}
				$statuses = $temp;
			}
		}

		// setup type meta query
		$types = array(
			'relation' => 'OR',
		);

		if ( 'any' === $type ) {
			$types[] = array(
				'key' => $this->meta_prefix . 'payment_type',
				'value' => 'recurring',
			);
			$types[] = array(
				'key' => $this->meta_prefix . 'payment_type',
				'value' => 'single',
			);
			$types[] = array(
				'key' => $this->meta_prefix . 'payment_type',
				'value' => 'trial',
			);
		} elseif ( is_string( $type ) ) {
			$types[] = array(
				'key' => $this->meta_prefix . 'payment_type',
				'value' => $type,
			);
		} elseif ( is_array( $type ) ) {
			foreach ( $type as $t ) {
				$types[] = array(
					'key' => $this->meta_prefix . 'payment_type',
					'value' => $t,
				);
			}
		}

		// execute the query
		$query = new WP_Query( apply_filters( 'llms_order_get_transactions_query', array(
			'meta_query' => array(
				'relation' => 'AND',
				array(
					'key' => $this->meta_prefix . 'order_id',
					'value' => $this->get( 'id' ),
				),
				$types,
			),
			'order' => $order,
			'orderby' => $orderby,
			'post_status' => $statuses,
			'post_type' => 'llms_transaction',
			'posts_per_page' => $per_page,
			'paged' => $paged,
		) ), $this, $status );

		$transactions = array();

		foreach ( $query->posts as $post ) {
			$transactions[ $post->ID ] = llms_get_post( $post );
		}

		return array(
			'count' => count( $query->posts ),
			'page' => $paged,
			'pages' => $query->max_num_pages,
			'transactions' => $transactions,
		);

	}

	/**
	 * Retrieve the date when a trial will end
	 * @param    string     $format  date return format
	 * @return   string
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function get_trial_end_date( $format = 'Y-m-d H:i:s' ) {

		if ( ! $this->has_trial() ) {

			$trial_end_date = '';

		} else {

			// retrieve the saved end date
			$trial_end_date = $this->get_date( 'date_trial_end', $format );

			// if not saved, calculate it
			if ( ! $trial_end_date ) {

				$trial_end_date = $this->calculate_trial_end_date( $format );

			}
		}

		return apply_filters( 'llms_order_get_trial_end_date', $trial_end_date, $this );

	}

	/**
	 * Gets the total revenue of an order
	 * @param    string     $type    revenue type [grosse|net]
	 * @return   float
	 * @since    3.0.0
	 * @version  3.1.3 - handle legacy orders
	 */
	public function get_revenue( $type = 'net' ) {

		if ( $this->is_legacy() ) {

			$amount = $this->get( 'total' );

		} else {

			$amount = $this->get_transaction_total( 'amount' );

			if ( 'net' === $type ) {

				$refunds = $this->get_transaction_total( 'refund_amount' );

				$amount = $amount - $refunds;

			}
		}

		return apply_filters( 'llms_order_get_revenue' , $amount, $type, $this );

	}

	/**
	 * Get a link to view the order on the student dashboard
	 * @return   string
	 * @since    3.0.0
	 * @version  3.8.0
	 */
	public function get_view_link() {

		$link = llms_get_endpoint_url( 'orders', $this->get( 'id' ), llms_get_page_url( 'myaccount' ) );
		return apply_filters( 'llms_order_get_view_link', $link, $this );

	}

	/**
	 * Determine if the student associated with this order has access
	 * @return   boolean
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function has_access() {
		return ( 'active' === $this->get_access_status() ) ? true : false;
	}

	/**
	 * Determine if a coupon was used
	 * @return   boolean
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function has_coupon() {
		return ( 'yes' === $this->get( 'coupon_used' ) );
	}

	/**
	 * Determine if there was a discount applied to this order
	 * via either a sale or a coupon
	 * @return   boolean
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function has_discount() {
		return ( $this->has_coupon() || $this->has_sale() );
	}

	/**
	 * Determine if the access plan was on sale during the purchase
	 * @return   boolean
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function has_sale() {
		return ( 'yes' === $this->get( 'on_sale' ) );
	}

	/**
	 * Determine if there's a payment scheduled for the order
	 * @return   boolean
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function has_scheduled_payment() {
		$date = $this->get_next_payment_due_date();
		return is_wp_error( $date ) ? false : true;
	}

	/**
	 * Determine if the order has a trial
	 * @return   boolean     true if has a trial, false if it doesn't
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function has_trial() {
		return ( $this->is_recurring() && 'yes' === $this->get( 'trial_offer' ) );
	}

	/**
	 * Determine if the trial period has ended for the order
	 * @return   boolean     true if ended, false if not ended
	 * @since    3.0.0
	 * @version  3.10.0
	 */
	public function has_trial_ended() {
		return ( llms_current_time( 'timestamp' ) >= $this->get_trial_end_date( 'U' ) );
	}

	/**
	 * Initialize a pending order
	 * Used during checkout
	 * Assumes all data passed in has already been validated
	 * @param    obj     $person   LLMS_Student
	 * @param    obj     $plan     LLMS_Access_Plan
	 * @param    obj     $gateway  LLMS_Gateway
	 * @param    mixed   $coupon   LLMS_Coupon or false
	 * @return   obj               $this
	 * @since    3.8.0
	 * @version  3.10.0
	 */
	public function init( $person, $plan, $gateway, $coupon = false ) {

		// user related information
		$this->set( 'user_id', $person->get_id() );
		$this->set( 'user_ip_address', llms_get_ip_address() );
		$this->set( 'billing_address_1', $person->get( 'billing_address_1' ) );
		$this->set( 'billing_address_2', $person->get( 'billing_address_2' ) );
		$this->set( 'billing_city', $person->get( 'billing_city' ) );
		$this->set( 'billing_country', $person->get( 'billing_country' ) );
		$this->set( 'billing_email', $person->get( 'user_email' ) );
		$this->set( 'billing_first_name', $person->get( 'first_name' ) );
		$this->set( 'billing_last_name', $person->get( 'last_name' ) );
		$this->set( 'billing_state', $person->get( 'billing_state' ) );
		$this->set( 'billing_zip', $person->get( 'billing_zip' ) );
		$this->set( 'billing_phone', $person->get( 'phone' ) );

		// access plan data
		$this->set( 'plan_id', $plan->get( 'id' ) );
		$this->set( 'plan_title', $plan->get( 'title' ) );
		$this->set( 'plan_sku', $plan->get( 'sku' ) );

		// product data
		$product = $plan->get_product();
		$this->set( 'product_id', $product->get( 'id' ) );
		$this->set( 'product_title', $product->get( 'title' ) );
		$this->set( 'product_sku', $product->get( 'sku' ) );
		$this->set( 'product_type', $plan->get_product_type() );

		$this->set( 'payment_gateway', $gateway->get_id() );
		$this->set( 'gateway_api_mode', $gateway->get_api_mode() );

		// trial data
		if ( $plan->has_trial() ) {
			$this->set( 'trial_offer', 'yes' );
			$this->set( 'trial_length', $plan->get( 'trial_length' ) );
			$this->set( 'trial_period', $plan->get( 'trial_period' ) );
			$trial_price = $plan->get_price( 'trial_price', array(), 'float' );
			$this->set( 'trial_original_total', $trial_price );
			$trial_total = $coupon ? $plan->get_price_with_coupon( 'trial_price', $coupon, array(), 'float' ) : $trial_price;
			$this->set( 'trial_total', $trial_total );
			$this->set( 'date_trial_end', $this->calculate_trial_end_date() );
		} else {
			$this->set( 'trial_offer', 'no' );
		}

		$price = $plan->get_price( 'price', array(), 'float' );
		$this->set( 'currency', get_lifterlms_currency() );

		// price data
		if ( $plan->is_on_sale() ) {
			$price_key = 'sale_price';
			$this->set( 'on_sale', 'yes' );
			$sale_price = $plan->get( 'sale_price', array(), 'float' );
			$this->set( 'sale_price', $sale_price );
			$this->set( 'sale_value', $price - $sale_price );
		} else {
			$price_key = 'price';
			$this->set( 'on_sale', 'no' );
		}

		// store original total before any discounts
		$this->set( 'original_total', $price );

		// get the actual total due after discounts if any are applicable
		$total = $coupon ? $plan->get_price_with_coupon( $price_key, $coupon, array(), 'float' ) : $$price_key;
		$this->set( 'total', $total );

		// coupon data
		if ( $coupon ) {
			$this->set( 'coupon_id', $coupon->get( 'id' ) );
			$this->set( 'coupon_amount', $coupon->get( 'coupon_amount' ) );
			$this->set( 'coupon_code', $coupon->get( 'title' ) );
			$this->set( 'coupon_type', $coupon->get( 'discount_type' ) );
			$this->set( 'coupon_used', 'yes' );
			$this->set( 'coupon_value', $$price_key - $total );
			if ( $plan->has_trial() && $coupon->has_trial_discount() ) {
				$this->set( 'coupon_amount_trial', $coupon->get( 'trial_amount' ) );
				$this->set( 'coupon_value_trial', $trial_price - $trial_total );
			}
		} else {
			$this->set( 'coupon_used', 'no' );
		}

		// get all billing schedule related information
		$this->set( 'billing_frequency', $plan->get( 'frequency' ) );
		if ( $plan->is_recurring() ) {
			$this->set( 'billing_length', $plan->get( 'length' ) );
			$this->set( 'billing_period', $plan->get( 'period' ) );
			$this->set( 'order_type', 'recurring' );
			if ( $plan->get( 'length' ) ) {
				$this->set( 'date_billing_end', date_i18n( 'Y-m-d H:i:s', $this->calculate_billing_end_date() ) );
			}
			$this->set( 'date_next_payment', $this->calculate_next_payment_date() );
		} else {
			$this->set( 'order_type', 'single' );
		}

		$this->set( 'access_expiration', $plan->get( 'access_expiration' ) );

		// get access related data so when payment is complete we can calculate the actual expiration date
		if ( $plan->can_expire() ) {
			$this->set( 'access_expires', $plan->get( 'access_expires' ) );
			$this->set( 'access_length', $plan->get( 'access_length' ) );
			$this->set( 'access_period', $plan->get( 'access_period' ) );
		}

		do_action( 'lifterlms_new_pending_order', $this, $person );

		return $this;

	}

	/**
	 * Determine if the order is a legacy order migrated from 2.x
	 * @return   boolean
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function is_legacy() {
		return ( 'publish' === $this->get( 'status' ) );
	}

	/**
	 * Determine if the order is recurring or singular
	 * @return   boolean      true if recurring, false if not
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function is_recurring() {
		return $this->get( 'order_type' ) === 'recurring';
	}

	/**
	 * Schedule access expiration
	 *
	 * @since 3.19.0
	 * @since 3.32.0 Update to use latest action-scheduler functions.
	 *
	 * @return void
	 */
	public function maybe_schedule_expiration() {

		// get expiration date based on setting.
		$expires = $this->get_access_expiration_date( 'U' );

		// will return a timestamp or "Lifetime Access as a string".
		if ( is_numeric( $expires ) ) {
			$this->unschedule_expiration();
			as_schedule_single_action( $expires, 'llms_access_plan_expiration', $this->get_action_args() );
		}

	}

	/**
	 * Schedules the next payment due on a recurring order
	 *
	 * Can be called without consequence on a single payment order
	 * Will always unschedule the scheduled action (if one exists) before scheduling another
	 *
	 * @since 3.0.0
	 * @since 3.32.0 Update to use latest action-scheduler functions.
	 *
	 * @return void
	 */
	public function maybe_schedule_payment( $recalc = true ) {

		if ( ! $this->is_recurring() ) {
			return;
		}

		if ( $recalc ) {
			$this->set( 'date_next_payment', $this->calculate_next_payment_date() );
		}

		$date = $this->get_next_payment_due_date( 'U' );

		// unschedule and reschedule
		if ( $date && ! is_wp_error( $date ) ) {

			// unschedule the next action (does nothing if no action scheduled)
			$this->unschedule_recurring_payment();

			// convert our date to UTC before passing to the scheduler
			$date = $date - ( HOUR_IN_SECONDS * get_option( 'gmt_offset' ) );

			// schedule the payment
			as_schedule_single_action( $date, 'llms_charge_recurring_payment', array(
				'order_id' => $this->get( 'id' ),
			) );

		} elseif ( is_wp_error( $date ) ) {

			if ( 'plan-ended' === $date->get_error_code() ) {

				// unschedule the next action (does nothing if no action scheduled)
				$this->unschedule_recurring_payment();

				// add a note that the plan has completed
				$this->add_note( __( 'Order payment plan completed.', 'lifterlms' ) );

			}
		}

	}

	/**
	 * Handles scheduling recurring payment retries when the gateway supports them
	 * @return   void
	 * @since    3.10.0
	 * @version  3.10.0
	 */
	public function maybe_schedule_retry() {

		if ( ! $this->can_be_retried() ) {
			return;
		}

		$current_rule = $this->get( 'last_retry_rule' );
		if ( '' === $current_rule ) {
			$current_rule = 0;
		} else {
			$current_rule = $current_rule + 1;
		}
		$rules = $this->get_retry_rules();

		if ( isset( $rules[ $current_rule ] ) ) {

			$rule = $rules[ $current_rule ];

			$next_payment_time = current_time( 'timestamp' ) + $rule['delay'];

			// update the status
			$this->set_status( $rule['status'] );

			// set the next payment date based on the rule's delay
			$this->set_date( 'next_payment', date_i18n( 'Y-m-d H:i:s', $next_payment_time ) );

			// save the rule for reference on potential future retries
			$this->set( 'last_retry_rule', $current_rule );

			// if notifications should be sent, trigger them
			if ( $rule['notifications'] ) {
				do_action( 'llms_send_automatic_payment_retry_notification', $this );
			}

			$this->add_note( sprintf( esc_html__( 'Automatic retry attempt scheduled for %s', 'lifterlms' ), date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $next_payment_time ) ) );

			// generic action
			do_action( 'llms_automatic_payment_retry_scheduled', $this );

			// we are out of rules, fail the order, move on with our lives
		} else {

			$this->set_status( 'failed' );
			$this->set( 'last_retry_rule', '' );

			$this->add_note( esc_html__( 'Maximum retry attempts reached.', 'lifterlms' ) );

			do_action( 'llms_automatic_payment_maximum_retries_reached', $this );

		}// End if().

	}

	/**
	 * Record a transaction for the order
	 * @param    array      $data    optional array of additional data to store for the transaction
	 * @return   obj        instance of LLMS_Transaction for the created transaction
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function record_transaction( $data = array() ) {

		extract( array_merge(
			array(
				'amount' => 0,
				'completed_date' => current_time( 'mysql' ),
				'customer_id' => '',
				'fee_amount' => 0,
				'source_id' => '',
				'source_description' => '',
				'transaction_id' => '',
				'status' => 'llms-txn-succeeded',
				'payment_gateway' => $this->get( 'payment_gateway' ),
				'payment_type' => 'single',
			),
			$data
		) );

		$txn = new LLMS_Transaction( 'new', $this->get( 'id' ) );

		$txn->set( 'api_mode', $this->get( 'gateway_api_mode' ) );
		$txn->set( 'amount', $amount );
		$txn->set( 'currency', $this->get( 'currency' ) );
		$txn->set( 'gateway_completed_date', date_i18n( 'Y-m-d h:i:s', strtotime( $completed_date ) ) );
		$txn->set( 'gateway_customer_id', $customer_id );
		$txn->set( 'gateway_fee_amount', $fee_amount );
		$txn->set( 'gateway_source_id', $source_id );
		$txn->set( 'gateway_source_description', $source_description );
		$txn->set( 'gateway_transaction_id', $transaction_id );
		$txn->set( 'order_id', $this->get( 'id' ) );
		$txn->set( 'payment_gateway', $payment_gateway );
		$txn->set( 'payment_type', $payment_type );
		$txn->set( 'status', $status );

		return $txn;

	}

	/**
	 * Date field setter for date fields that require things to be updated when their value changes
	 * This is mainly used to allow updating dates which are editable from the admin panel which
	 * should trigger additional actions when updated
	 *
	 * Settable dates: date_next_payment, date_trial_end, date_access_expires
	 *
	 * @param    string     $date_key  date field to set
	 * @param    string     $date_val  date string or a unix time stamp
	 * @since    3.10.0
	 * @version  3.19.0
	 */
	public function set_date( $date_key, $date_val ) {

		// convert to timestamp if not already a timestamp
		if ( ! is_numeric( $date_val ) ) {
			$date_val = strtotime( $date_val );
		}

		$this->set( 'date_' . $date_key, date( 'Y-m-d H:i:s', $date_val ) );

		switch ( $date_key ) {

			// reschedule access expiration
			case 'access_expires':
				$this->maybe_schedule_expiration();
			break;

			// additionally update the next payment date
			// & don't break because we want to reschedule payments too
			case 'trial_end':
				$this->set_date( 'next_payment', $this->calculate_next_payment_date( 'U' ) );

				// everything else reschedule's payments
			default:
				$this->maybe_schedule_payment( false );

		}

	}

	/**
	 * Update the status of an order
	 * @param    string     $status  status name, accepts unprefixed statuses
	 * @return   void
	 * @since    3.8.0
	 * @version  3.10.0
	 */
	public function set_status( $status ) {

		if ( false === strpos( $status, 'llms-' ) ) {
			$status = 'llms-' . $status;
		}

		$statuses = array_keys( llms_get_order_statuses( $this->get( 'order_type' ) ) );

		if ( in_array( $status, $statuses ) ) {
			$this->set( 'status', $status );
		}

	}

	/**
	 * Record the start date of the access plan and schedule expiration
	 * if expiration is required in the future
	 * @return   void
	 * @since    3.0.0
	 * @version  3.19.0
	 */
	public function start_access() {

		// only start access if access isn't already started
		$date = $this->get( 'start_date' );
		if ( ! $date ) {

			// set the start date to now
			$date = llms_current_time( 'mysql' );
			$this->set( 'start_date', $date );

		}

		$this->unschedule_expiration();

		// setup expiration
		if ( in_array( $this->get( 'access_expiration' ), array( 'limited-date', 'limited-period' ) ) ) {

			$expires_date = $this->get_access_expiration_date( 'Y-m-d H:i:s' );
			$this->set( 'date_access_expires', $expires_date );
			$this->maybe_schedule_expiration();

		}

	}

	/**
	 * Cancels a scheduled expiration action
	 *
	 * Does nothing if no expiration is scheduled
	 *
	 * @since 3.19.0
	 * @since 3.32.0 Update to use latest action-scheduler functions.
	 *
	 * @return void
	 */
	public function unschedule_expiration() {

		if ( as_next_scheduled_action( 'llms_access_plan_expiration', $this->get_action_args() ) ) {
			as_unschedule_action( 'llms_access_plan_expiration', $this->get_action_args() );
		}

	}

	/**
	 * Cancels a scheduled recurring payment action
	 *
	 * Does nothing if no payments are scheduled
	 *
	 * @since 3.0.0
	 * @since 3.32.0 Update to use latest action-scheduler functions.
	 *
	 * @return void
	 */
	public function unschedule_recurring_payment() {

		if ( as_next_scheduled_action( 'llms_charge_recurring_payment', $this->get_action_args() ) ) {
			as_unschedule_action( 'llms_charge_recurring_payment', $this->get_action_args() );
		}

	}

}

Top ↑

Changelog Changelog

Changelog
Version Description
3.32.0 Update to use latest action-scheduler functions.
3.0.0 Introduced.


Top ↑

Methods Methods

  • add_note — Add an admin-only note to the order visible on the admin panel notes are recorded using the wp comments API & DB
  • after_create — Called after inserting a new order into the database
  • calculate_billing_end_date — Calculate the date when billing should applicable to orders created from plans with a set # of billing intervals
  • calculate_next_payment_date — Calculate the next payment due date
  • calculate_trial_end_date — Calculate the end date of the trial
  • can_be_retried — Determine if the order can be retried for recurring payments
  • can_resubscribe — Determine if an order can be resubscribed to
  • generate_order_key — Generate an order key for the order
  • get_access_expiration_date — Determine the date when access will expire based on the access settings of the access plan at the $start_date of access
  • get_access_status — Get the current status of a student's access based on the access plan data stored on the order at the time of purchase
  • get_action_args — Retrieve arguments passed to order-related events processed by the action scheduler
  • get_coupon_amount — Get the formatted coupon amount with a currency symbol or percentage
  • get_creation_args — An array of default arguments to pass to $this->create() when creating a new post
  • get_customer_name — Retrieve the customer's full name
  • get_gateway — Retrieve the payment gateway instance for the order's selected payment gateway
  • get_initial_price — Get the initial payment amount due on checkout This will always be the value of "total" except when the product has a trial
  • get_last_transaction — Retrieve the last (most recent) transaction processed for the order
  • get_last_transaction_date — Retrieve the date of the last (most recent) transaction
  • get_next_payment_due_date — Retrieve the due date of the next payment according to access plan terms
  • get_notes — Get an array of the order notes Each note is actually a WordPress comment
  • get_product — Retrieve an LLMS_Post_Model object for the associated product
  • get_retry_rules — Get configured payment retry rules
  • get_revenue — Gets the total revenue of an order
  • get_start_date — Get the start date for the order gets the date of the first initially successful transaction if none found, uses the created date of the order
  • get_transaction_total — SQL query to retrieve total amounts for transactions by type
  • get_transactions — Retrieve an array of transactions associated with the order according to supplied arguments
  • get_trial_end_date — Retrieve the date when a trial will end
  • get_view_link — Get a link to view the order on the student dashboard
  • has_access — Determine if the student associated with this order has access
  • has_coupon — Determine if a coupon was used
  • has_discount — Determine if there was a discount applied to this order via either a sale or a coupon
  • has_sale — Determine if the access plan was on sale during the purchase
  • has_scheduled_payment — Determine if there's a payment scheduled for the order
  • has_trial — Determine if the order has a trial
  • has_trial_ended — Determine if the trial period has ended for the order
  • init — Initialize a pending order Used during checkout Assumes all data passed in has already been validated
  • is_legacy — Determine if the order is a legacy order migrated from 2.x
  • is_recurring — Determine if the order is recurring or singular
  • maybe_schedule_expiration — Schedule access expiration
  • maybe_schedule_payment — Schedules the next payment due on a recurring order
  • maybe_schedule_retry — Handles scheduling recurring payment retries when the gateway supports them
  • record_transaction — Record a transaction for the order
  • set_date — Date field setter for date fields that require things to be updated when their value changes This is mainly used to allow updating dates which are editable from the admin panel which should trigger additional actions when updated
  • set_status — Update the status of an order
  • start_access — Record the start date of the access plan and schedule expiration if expiration is required in the future
  • unschedule_expiration — Cancels a scheduled expiration action
  • unschedule_recurring_payment — Cancels a scheduled recurring payment action

Top ↑

User Contributed Notes User Contributed Notes

You must log in before being able to contribute a note or feedback.





Permalink: