LLMS_Controller_Orders

LLMS_Controller_Orders class.


Description Description


Source Source

File: includes/controllers/class.llms.controller.orders.php

class LLMS_Controller_Orders {

	/**
	 * Constructor
	 *
	 * @since 3.0.0
	 * @since 3.19.0 Updated.
	 * @since 3.33.0 Added `before_delete_post` action to handle order deletion
	 */
	public function __construct() {

		// form actions
		add_action( 'init', array( $this, 'create_pending_order' ) );
		add_action( 'init', array( $this, 'confirm_pending_order' ) );
		add_action( 'init', array( $this, 'switch_payment_source' ) );

		// this action adds our lifterlms specific actions when order & transaction statuses change
		add_action( 'transition_post_status', array( $this, 'transition_status' ), 10, 3 );

		// this action adds lifterlms specific action when an order is deleted, just before the WP post postmetas are removed.
		add_action( 'before_delete_post', array( $this, 'on_delete_order' ) );

		/**
		 * Status Change Actions for Orders and Transactions
		 */

		// transaction status changes cascade up to the order to change the order status
		add_action( 'lifterlms_transaction_status_failed', array( $this, 'transaction_failed' ), 10, 1 );
		add_action( 'lifterlms_transaction_status_refunded', array( $this, 'transaction_refunded' ), 10, 1 );
		add_action( 'lifterlms_transaction_status_succeeded', array( $this, 'transaction_succeeded' ), 10, 1 );

		// status changes for orders to enroll students and trigger completion actions
		add_action( 'lifterlms_order_status_completed', array( $this, 'complete_order' ), 10, 2 );
		add_action( 'lifterlms_order_status_active', array( $this, 'complete_order' ), 10, 2 );

		// status changes to pending cancel
		add_action( 'lifterlms_order_status_pending-cancel', array( $this, 'pending_cancel_order' ), 10, 1 );

		// status changes for orders to unenroll students upon purchase
		add_action( 'lifterlms_order_status_refunded', array( $this, 'error_order' ), 10, 1 );
		add_action( 'lifterlms_order_status_cancelled', array( $this, 'error_order' ), 10, 1 );
		add_action( 'lifterlms_order_status_expired', array( $this, 'error_order' ), 10, 1 );
		add_action( 'lifterlms_order_status_failed', array( $this, 'error_order' ), 10, 1 );
		add_action( 'lifterlms_order_status_on-hold', array( $this, 'error_order' ), 10, 1 );
		add_action( 'lifterlms_order_status_trash', array( $this, 'error_order' ), 10, 1 );

		/**
		 * Scheduler Actions
		 */

		// charge recurring payments
		add_action( 'llms_charge_recurring_payment', array( $this, 'recurring_charge' ), 10, 1 );

		// expire access plans
		add_action( 'llms_access_plan_expiration', array( $this, 'expire_access' ), 10, 1 );

	}

	/**
	 * Confirm order form post
	 * User clicks confirm order or gateway determines the order is confirmed
	 *
	 * Executes payment gateway confirm order method and completes order.
	 * Redirects user to appropriate page / post
	 *
	 * @return void
	 *
	 * @since   3.0.0
	 * @version 3.4.0
	 */
	public function confirm_pending_order() {

		if ( 'POST' !== strtoupper( getenv( 'REQUEST_METHOD' ) ) || empty( $_POST['action'] ) || 'confirm_pending_order' !== $_POST['action'] || empty( $_POST['_wpnonce'] ) ) { return; }

		// nonce the post
		wp_verify_nonce( $_POST['_wpnonce'], 'confirm_pending_order' );

		// ensure we have an order key we can locate the order with
		$key = isset( $_POST['llms_order_key'] ) ? $_POST['llms_order_key'] : false;
		if ( ! $key ) {
			return llms_add_notice( __( 'Could not locate an order to confirm.', 'lifterlms' ), 'error' );
		}

		// lookup the order & return error if not found
		$order = llms_get_order_by_key( $key );
		if ( ! $order || ! $order instanceof LLMS_Order ) {
			return llms_add_notice( __( 'Could not locate an order to confirm.', 'lifterlms' ), 'error' );
		}

		// ensure the order is pending
		if ( 'llms-pending' !== $order->get( 'status' ) ) {
			return llms_add_notice( __( 'Only pending orders can be confirmed.', 'lifterlms' ), 'error' );
		}

		// get the gateway
		$gateway = LLMS()->payment_gateways()->get_gateway_by_id( $order->get( 'payment_gateway' ) );

		// pass the order to the gateway
		$gateway->confirm_pending_order( $order );

	}

	/**
	 * Perform actions on a successful order completion
	 * @param    obj    $order       Instance of an LLMS_Order
	 * @param    string $old_status  Previous order status (eg: 'pending')
	 * @return   void
	 * @since    1.0.0
	 * @version  3.19.0
	 */
	public function complete_order( $order, $old_status ) {

		// clear expiration date when moving from a pending-cancel order
		if ( 'pending-cancel' === $old_status ) {
			$order->set( 'date_access_expires', '' );
		}

		// record access start time & maybe schedule expiration
		$order->start_access();

		$order_id = $order->get( 'id' );
		$product_id = $order->get( 'product_id' );
		$user_id = $order->get( 'user_id' );

		unset( LLMS()->session->llms_coupon );

		// trigger order complete action
		do_action( 'lifterlms_order_complete', $order_id ); // @todo used by AffiliateWP only, can remove after updating AffiliateWP

		// enroll student
		llms_enroll_student( $user_id, $product_id, 'order_' . $order_id );

		// trigger purchase action, used by engagements
		do_action( 'lifterlms_product_purchased', $user_id, $product_id );
		do_action( 'lifterlms_access_plan_purchased', $user_id, $order->get( 'plan_id' ) );

		// maybe schedule a payment
		$order->maybe_schedule_payment();

	}


	/**
	 * Handle form submission of the checkout / payment form
	 *
	 * 		1. Logs in or Registers a user
	 *   	2. Validates all fields
	 *    	3. Handles coupon pricing adjustments
	 *    	4. Creates a PENDING llms_order
	 *
	 * 		If errors, returns error on screen to user
	 * 		If success, passes to the selected gateways "process_payment" method
	 * 			the process_payment method should complete by returning an error or
	 * 			triggering the "lifterlms_process_payment_redirect" // todo check this last statement
	 *
	 * @return void
	 * @since    3.0.0
	 * @version  3.27.0
	 */
	public function create_pending_order() {

		if ( ! llms_verify_nonce( '_llms_checkout_nonce', 'create_pending_order', 'POST' ) ) {
			return;
		}

		if ( empty( $_POST['action'] ) || 'create_pending_order' !== $_POST['action'] ) {
			return;
		}

		// prevent timeout
		@set_time_limit( 0 );

		/**
		 * Allow gateways, extensions, etc to do their own validation prior to standard validation
		 * If this returns a truthy, we'll stop processing
		 * The extension should add a notice in addition to returning the truthy
		 */
		if ( apply_filters( 'llms_before_checkout_validation', false ) ) {
			return;
		}

		// setup data to pass to the pending order creation function
		$data = array();
		$keys = array(
			'llms_plan_id',
			'llms_agree_to_terms',
			'llms_payment_gateway',
			'llms_coupon_code',
		);

		foreach ( $keys as $key ) {
			if ( isset( $_POST[ $key ] ) ) {
				$data[ str_replace( 'llms_', '', $key ) ] = $_POST[ $key ];
			}
		}

		$data['customer'] = array();
		if ( get_current_user_id() ) {
			$data['customer']['user_id'] = get_current_user_id();
		}
		foreach ( LLMS_Person_Handler::get_available_fields( 'checkout' ) as $cust_field ) {
			$cust_key = $cust_field['id'];
			if ( isset( $_POST[ $cust_key ] ) ) {
				$data['customer'][ $cust_key ] = $_POST[ $cust_key ];
			}
		}

		$setup = llms_setup_pending_order( $data );

		if ( is_wp_error( $setup ) ) {

			foreach ( $setup->get_error_messages() as $msg ) {
				llms_add_notice( $msg, 'error' );
			}

			// existing user fails validation from the free checkout form
			if ( get_current_user_id() && isset( $_POST['form'] ) && 'free_enroll' === $_POST['form'] && isset( $_POST['llms_plan_id'] ) ) {
				$plan = llms_get_post( $_POST['llms_plan_id'] );
				wp_redirect( $plan->get_checkout_url() );
				exit;
			}

			return;

		}

		/**
		 * Allow gateways, extensions, etc to do their own validation
		 * after all standard validations are successfully
		 * If this returns a truthy, we'll stop processing
		 * The extension should add a notice in addition to returning the truthy
		 */
		if ( apply_filters( 'llms_after_checkout_validation', false ) ) {
			return;
		}

		$order_id = 'new';

		// get order ID by Key if it exists
		if ( ! empty( $_POST['llms_order_key'] ) ) {
			$locate = llms_get_order_by_key( $_POST['llms_order_key'], 'id' );
			if ( $locate ) {
				$order_id = $locate;
			}
		}

		// instantiate the order
		$order = new LLMS_Order( $order_id );

		// if there's no id we can't proceed, return an error
		if ( ! $order->get( 'id' ) ) {
			return llms_add_notice( __( 'There was an error creating your order, please try again.', 'lifterlms' ), 'error' );
		}

		// add order key to globals so the order can be retried if processing errors occur
		$_POST['llms_order_key'] = $order->get( 'order_key' );

		$order->init( $setup['person'], $setup['plan'], $setup['gateway'], $setup['coupon'] );

		// pass to the gateway to start processing
		$setup['gateway']->handle_pending_order( $order, $setup['plan'], $setup['person'], $setup['coupon'] );

	}

	/**
	 * Called when an order's status changes to refunded, cancelled, expired, or failed
	 *
	 * @param    obj    $order  instance of an LLMS_Order
	 * @return   void
	 *
	 * @since    3.0.0
	 * @version  3.10.0
	 */
	public function error_order( $order ) {

		switch ( current_filter() ) {

			case 'lifterlms_order_status_trash':
			case 'lifterlms_order_status_cancelled':
			case 'lifterlms_order_status_on-hold':
			case 'lifterlms_order_status_refunded':
				$status = 'cancelled';
			break;

			case 'lifterlms_order_status_expired':
			case 'lifterlms_order_status_failed':
			default:
				$status = 'expired';
			break;

		}

		$order->unschedule_recurring_payment();

		llms_unenroll_student( $order->get( 'user_id' ), $order->get( 'product_id' ), $status, 'order_' . $order->get( 'id' ) );

	}

	/**
	 * Called when a post is permanently deleted.
	 * Will delete any enrollment records linked to the LLMS_Order with the ID of the deleted post
	 *
	 * @since 3.33.0
	 *
	 * @param int $post_id WP_Post ID.
	 * @return void
	 */
	public function on_delete_order( $post_id ) {

		$order = llms_get_post( $post_id );
		if ( $order && is_a( $order, 'LLMS_Order' ) ) {
			llms_delete_student_enrollment( $order->get( 'user_id' ), $order->get( 'product_id' ), 'order_' . $order->get( 'id' ) );
		}

	}

	/**
	 * Handle expiration & cancellation from a course / membership
	 * Called via scheduled action set during order completion for plans with a limited access plan
	 * Additionally called when an order is marked as "pending-cancel" to revoke access at the end of a pre-paid period
	 * @param    int  $order_id  WP Post ID of the LLMS Order
	 * @return   void
	 * @since    3.0.0
	 * @version  3.19.0
	 */
	public function expire_access( $order_id ) {

		$order = new LLMS_Order( $order_id );
		$new_order_status = false;

		// pending cancel moves to cancelled
		if ( 'llms-pending-cancel' === $order->get( 'status' ) ) {

			$status = 'cancelled';
			$note = __( 'Student unenrolled at the end of access period due to subscription cancellation.', 'lifterlms' );
			$new_order_status = 'cancelled';

			// all others move to expired
		} else {

			$status = 'expired';
			$note = __( 'Student unenrolled due to automatic access plan expiration', 'lifterlms' );

		}

		llms_unenroll_student( $order->get( 'user_id' ), $order->get( 'product_id' ), $status, 'order_' . $order->get( 'id' ) );
		$order->add_note( $note );
		$order->unschedule_recurring_payment();

		if ( $new_order_status ) {
			$order->set_status( $new_order_status );
		}

	}

	/**
	 * Unschedule recurring payments and schedule access expiration
	 * @param    obj        $order  LLMS_Order object
	 * @return   void
	 * @since    3.19.0
	 * @version  3.19.0
	 */
	public function pending_cancel_order( $order ) {

		$date = $order->get_next_payment_due_date( 'Y-m-d H:i:s' );
		$order->set( 'date_access_expires', $date );

		$order->unschedule_recurring_payment();
		$order->maybe_schedule_expiration();

	}

	/**
	 * Trigger a recurring payment
	 *
	 * Called by action scheduler.
	 *
	 * @since 3.0.0
	 * @since 3.32.0 Record order notes and trigger actions during errors.
	 *
	 * @param int $order_id WP Post ID of the order.
	 * @return void
	 */
	public function recurring_charge( $order_id ) {

		$order = new LLMS_Order( $order_id );
		$gateway = $order->get_gateway();

		// ensure the gateway is still installed & available
		if ( ! is_wp_error( $gateway ) ) {

			if ( ! $gateway->supports( 'recurring_payments' ) ) {

				do_action( 'llms_order_recurring_charge_gateway_payments_disabled', $order_id, $gateway, $this );

				llms_log( 'Recurring charge for order # ' . $order_id . ' could not be processed because the gateway no longer supports recurring payments', 'recurring-payments' );

				$order->add_note( __( 'Recurring charge skipped because recurring payments are disabled in for the payment gateway.', 'lifterlms' ) );

			} elseif ( ! LLMS_Site::get_feature( 'recurring_payments' ) ) {

				do_action( 'llms_order_recurring_charge_skipped', $order_id, $gateway, $this );
				$order->add_note( __( 'Recurring charge skipped because recurring payments are disabled in staging mode.', 'lifterlms' ) );

			} else {

				$gateway->handle_recurring_transaction( $order );

			}
		} else {

			do_action( 'llms_order_recurring_charge_gateway_error', $order_id, $gateway, $this );

			llms_log( 'Recurring charge for order # ' . $order_id . ' could not be processed', 'recurring-payments' );
			llms_log( $gateway->get_error_message(), 'recurring-payments' );

			$order->add_note(
				sprintf(
					__( 'A recurring charge was not processed due to an error encountered while loading the payment gateway: "%s"', 'lifterlms' ),
					$gateway->get_error_message()
				)
			);

		}// End if().

	}


	/**
	 * Handle form submission of the "Update Payment Method" form on the student dashboard when viewing a single order
	 * @return   void
	 * @since    3.10.0
	 * @version  3.19.0
	 */
	public function switch_payment_source() {

		// invalid nonce or the form wasn't submitted
		if ( ! llms_verify_nonce( '_switch_source_nonce', 'llms_switch_order_source', 'POST' ) ) {
			return;
		} elseif ( ! isset( $_POST['order_id'] ) && ! is_numeric( $_POST['order_id'] ) && 0 == $_POST['order_id'] ) {
			return llms_add_notice( __( 'Missing order information.', 'lifterlms' ), 'error' );
		}

		$order = llms_get_post( $_POST['order_id'] );
		if ( ! $order || get_current_user_id() != $order->get( 'user_id' ) ) {
			return llms_add_notice( __( 'Invalid Order.', 'lifterlms' ), 'error' );
		} elseif ( empty( $_POST['llms_payment_gateway'] ) ) {
			return llms_add_notice( __( 'Missing gateway information.', 'lifterlms' ), 'error' );
		}

		$plan = llms_get_post( $order->get( 'plan_id' ) );
		$gateway_id = sanitize_text_field( $_POST['llms_payment_gateway'] );
		$gateway = $this->validate_selected_gateway( $gateway_id, $plan );

		if ( is_wp_error( $gateway ) ) {
			return llms_add_notice( $gateway->get_error_message(), 'error' );
		}

		// handoff to the gateway
		$gateway->handle_payment_source_switch( $order, $_POST );

		// if the order is pending cancel and there were no errors returned activate it
		if ( 'llms-pending-cancel' === $order->get( 'status' ) && ! llms_notice_count( 'error' ) ) {
			$order->set_status( 'active' );
		}

	}

	/**
	 * When a transaction fails, update the parent order's status
	 * @param    obj     $txn  Instance of the LLMS_Transaction
	 * @return   void
	 * @since    3.0.0
	 * @version  3.10.0
	 */
	public function transaction_failed( $txn ) {

		$order = $txn->get_order();

		// halt if legacy
		if ( $order->is_legacy() ) { return; }

		if ( $order->can_be_retried() ) {

			$order->maybe_schedule_retry();

		} else {

			$order->set( 'status', 'llms-failed' );

		}

	}

	/**
	 * When a transaction is refunded, update the parent order's status
	 * @param    obj     $txn  Instance of the LLMS_Transaction
	 * @return   void
	 * @since    3.0.0
	 * @version  3.0.0
	 */
	public function transaction_refunded( $txn ) {

		$order = $txn->get_order();

		// halt if legacy
		if ( $order->is_legacy() ) { return; }

		$order->set( 'status', 'llms-refunded' );

	}

	/**
	 * When a transaction succeeds, update the parent order's status
	 * @param    obj     $txn  Instance of the LLMS_Transaction
	 * @return   void
	 * @since    3.0.0
	 * @version  3.10.0
	 */
	public function transaction_succeeded( $txn ) {

		// get the order
		$order = $txn->get_order();

		// halt if legacy
		if ( $order->is_legacy() ) { return; }

		// update the status based on the order type
		$status = $order->is_recurring() ? 'llms-active' : 'llms-completed';
		$order->set( 'status', $status );
		$order->set( 'last_retry_rule', '' ); // retries should always start with tne first rule for new transactions

		// maybe schedule a payment
		$order->maybe_schedule_payment();

	}

	/**
	 * Trigger actions when the status of LifterLMS Orders and LifterLMS Transactions change status
	 * @param    string     $new_status  new status
	 * @param    string     $old_status  old status
	 * @param    ojb        $post        WP_Post instance
	 * @return   void
	 * @since    3.0.0
	 * @version  3.19.0
	 */
	public function transition_status( $new_status, $old_status, $post ) {

		// don't do anything if the status hasn't changed
		if ( $new_status === $old_status ) {
			return;
		}

		// we're only concerned with order post statuses here
		if ( 'llms_order' !== $post->post_type && 'llms_transaction' !== $post->post_type ) {
			return;
		}

		$post_type = str_replace( 'llms_', '', $post->post_type );
		$obj = 'order' === $post_type ? new LLMS_Order( $post ) : new LLMS_Transaction( $post );

		// record order status changes as notes
		if ( 'order' === $post_type ) {
			$obj->add_note( sprintf( __( 'Order status changed from %1$s to %2$s', 'lifterlms' ), llms_get_order_status_name( $old_status ), llms_get_order_status_name( $new_status ) ) );
		}

		// remove prefixes from all the things
		$new_status = str_replace( array( 'llms-', 'txn-' ), '', $new_status );
		$old_status = str_replace( array( 'llms-', 'txn-' ), '', $old_status );

		do_action( 'lifterlms_' . $post_type . '_status_' . $old_status . '_to_' . $new_status, $obj, $old_status, $new_status );
		do_action( 'lifterlms_' . $post_type . '_status_' . $new_status, $obj, $old_status, $new_status );

	}

	/**
	 * Validate a gateway can be used to process the current action / transaction
	 * @param    string     $gateway_id  gateway's id
	 * @param    obj        $plan        instance of the LLMS_Access_Plan related to the action/transaction
	 * @return   mixed                   WP_Error or LLMS_Payment_Gateway subclass
	 * @since    3.10.0
	 * @version  3.10.0
	 */
	private function validate_selected_gateway( $gateway_id, $plan ) {

		$gateway = LLMS()->payment_gateways()->get_gateway_by_id( $gateway_id );
		$err = new WP_Error();

		// valid gateway
		if ( is_subclass_of( $gateway, 'LLMS_Payment_Gateway' ) ) {

			// gateway not enabled
			if ( 'manual' !== $gateway->get_id() && ! $gateway->is_enabled() ) {

				return $err->add( 'gateway-error', __( 'The selected payment gateway is not currently enabled.', 'lifterlms' ) );

				// it's a recurring plan and the gateway doesn't support recurring
			} elseif ( $plan->is_recurring() && ! $gateway->supports( 'recurring_payments' ) ) {

				return $err->add( 'gateway-error', sprintf( __( '%s does not support recurring payments and cannot process this transaction.', 'lifterlms' ), $gateway->get_title() ) );

				// not recurring and the gateway doesn't support single payments
			} elseif ( ! $plan->is_recurring() && ! $gateway->supports( 'single_payments' ) ) {

				return $err->add( 'gateway-error', sprintf( __( '%s does not support single payments and cannot process this transaction.', 'lifterlms' ), $gateway->get_title() ) );

			}
		} else {

			return $err->add( 'invalid-gateway', __( 'An invalid payment method was selected.', 'lifterlms' ) );

		}

		return $gateway;

	}

}

Top ↑

Changelog Changelog

Changelog
Version Description
3.33.0 Added logic to delete any enrollment records linked to an LLMS_Order on its permanent deletion.
3.0.0 Introduced.

Top ↑

Methods Methods

  • __construct — Constructor
  • complete_order — Perform actions on a successful order completion
  • confirm_pending_order — Confirm order form post User clicks confirm order or gateway determines the order is confirmed
  • create_pending_order — Handle form submission of the checkout / payment form
  • error_order — Called when an order's status changes to refunded, cancelled, expired, or failed
  • expire_access — Handle expiration & cancellation from a course / membership Called via scheduled action set during order completion for plans with a limited access plan Additionally called when an order is marked as "pending-cancel" to revoke access at the end of a pre-paid period
  • on_delete_order — Called when a post is permanently deleted.
  • pending_cancel_order — Unschedule recurring payments and schedule access expiration
  • recurring_charge — Trigger a recurring payment
  • switch_payment_source — Handle form submission of the "Update Payment Method" form on the student dashboard when viewing a single order
  • transaction_failed — When a transaction fails, update the parent order's status
  • transaction_refunded — When a transaction is refunded, update the parent order's status
  • transaction_succeeded — When a transaction succeeds, update the parent order's status
  • transition_status — Trigger actions when the status of LifterLMS Orders and LifterLMS Transactions change status
  • validate_selected_gateway — Validate a gateway can be used to process the current action / transaction

Top ↑

User Contributed Notes User Contributed Notes

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





Permalink: