LLMS_Order_Generator
Validate and create LLMS_Order posts.
Contents
Source Source
File: includes/class-llms-order-generator.php
class LLMS_Order_Generator { /** * Error code: invalid coupon code submitted. * * @var string */ const E_COUPON_INVALID = 'llms-order-gen-coupon-invalid'; /** * Error code: coupon code not found. * * @var string */ const E_COUPON_NOT_FOUND = 'llms-order-gen-coupon-not-found'; /** * Error code: issue encountered during order post creation. * * @var string */ const E_CREATE_ORDER = 'llms-order-gen-create-order'; /** * Error code: payment gateway id not submitted. * * @var string */ const E_GATEWAY_REQUIRED = 'llms-order-gen-gateway-required'; /** * Error code: missing or invalid order key during confirmation. * * @var string */ const E_ORDER_NOT_FOUND = 'llms-order-gen-order-not-found'; /** * Error code: order cannot be confirmed. * * @var string */ const E_ORDER_NOT_CONFIRMABLE = 'llms-order-gen-order-not-confirmable'; /** * Error code: required plan ID not submitted. * * @var string */ const E_PLAN_REQUIRED = 'llms-order-gen-plan-required'; /** * Error code: access plan not found. * * @var string */ const E_PLAN_NOT_FOUND = 'llms-order-gen-plan-not-found'; /** * Error code: site's terms not accepted. * * @var string */ const E_SITE_TERMS = 'llms-order-gen-site-terms'; /** * Error code: user already enrolled. * * @var string */ const E_USER_ENROLLED = 'llms-order-gen-user-enrolled'; /** * User Action: validate and then commit (register or update) the user. * * @var string */ const UA_COMMIT = 'commit'; /** * User Action: perform user validation only. * * @var string */ const UA_VALIDATE = 'validate'; /** * The coupon used to discount the order. * * Derived from `$this->data['llms_coupon_code']`. * * Will be empty until the coupon is validated. * * @var LLMS_Coupon|null */ protected $coupon = null; /** * Associative array of input data. * * Usually the $_POST superglobal. * * @var array */ protected $data = array(); /** * The payment gateway used to process the order. * * Derived from `$this->data['llms_payment_gateway']` . * * Will be empty until the gateway is validated. * * @var LLMS_Payment_Gateway|null */ protected $gateway = null; /** * The access plan used to generate the order. * * Derived from `$this->data['llms_plan_id']`. * * Will be empty until the plan is validated. * * @var LLMS_Access_Plan|null */ protected $plan = null; /** * The order. * * Derived from `$this->data['llms_order_key']`. * * Will be empty until the order is validated. * * This is only used during confirmation of existing orders. * * @var LLMS_Order|null */ protected $order = null; /** * The student used to generate the order. * * Will be empty until the user is created / update following all validations. * * @var LLMS_Student|null */ protected $student = null; /** * Constructor. * * @since 7.0.0 * * @param array $data { * An associative array of input data used to generate the order, usually from $_POST. * * @type integer $llms_plan_id An LLMS_Access_Plan ID. * @type string $llms_agree_to_terms A yes/no value determining whether or not the user has agreed to the site's terms. * @type string $llms_payment_gateway The ID of the payment gateway used to process the order. * @type string $llms_coupon_code Optional. The coupon code string being used. * @type string $llms_order_key Optional. An `LLMS_Order` key used to modify an existing pending order rather than creating a new one. * @type array ...$user_data All remaining data is passed to the user creation functions. * } * @return void */ public function __construct( $data ) { $this->data = $data; } /** * Confirms an existing pending order. * * @since 7.0.0 * * @return WP_Error|array Returns an array of data from the payment gateway's `confirm_pending_order()` method on success. */ public function confirm() { $validate = $this->validate( true ); if ( is_wp_error( $validate ) ) { return $validate; } $gateway_confirm = $this->gateway->confirm_pending_order( $this->order ); if ( is_wp_error( $gateway_confirm ) ) { return $gateway_confirm; } $user = $this->commit_user(); if ( is_wp_error( $user ) ) { return $user; } // Save the user to the order. $this->order->set_user_data( $this->get_user_data() ); if ( 'SUCCESS' === ( $gateway_confirm['status'] ?? null ) && ! empty( $gateway_confirm['transaction'] ) ) { // Record the transaction. $this->order->record_transaction( $gateway_confirm['transaction'] ); } return $gateway_confirm; } /** * Creates a new pending order. * * @since 7.0.0 * * @return WP_Error|LLMS_Order */ protected function create() { $order = new LLMS_Order( $this->get_order_id() ); // If there's no id we can't proceed, return an error. if ( ! $order->get( 'id' ) ) { return $this->error( self::E_CREATE_ORDER, __( 'There was an error creating your order, please try again.', 'lifterlms' ) ); } $order->init( $this->get_user_data(), $this->plan, $this->gateway, $this->coupon ); return $order; } /** * Registers or updates the user from the submitted data. * * @since 7.0.0 * * @return integer|WP_Error Returns the `WP_User` ID on success or an error object. */ protected function commit_user() { $args = array( 'plan' => $this->plan, ); $user_id = get_current_user_id() ? llms_update_user( $this->data, 'checkout', $args ) : llms_register_user( $this->data, 'checkout', true, $args ); if ( ! is_wp_error( $user_id ) ) { $this->student = llms_get_student( $user_id ); } return $user_id; } /** * Returns an error object. * * This method accepts an error code and message and passes them directly to `WP_Error` and * adds all class variables to the error objects `$data` parameter. * * @since 7.0.0 * * @param string $code Error code. * @param string $message Error message. * @param array $extra_data Additional data to pass to WP_Error's 3rd parameter. * @return WP_Error */ protected function error( $code, $message, $extra_data = array() ) { $data = get_class_vars( __CLASS__ ); foreach ( $data as $key => &$val ) { $val = $this->{$key}; } return new WP_Error( $code, $message, array_merge( $data, $extra_data ) ); } /** * Attempts to locate a user ID. * * Uses the logged in user's information and falls back to a lookup by email address if available. * * @since 7.0.0 * * @param string|null $email An email address, if available. * @return null|integer Returns the WP_User ID or null if not found. */ private function find_user_id( $email = null ) { if ( is_user_logged_in() ) { return get_current_user_id(); } if ( $email ) { $user = get_user_by( 'email', $email ); return $user ? $user->ID : null; } return null; } /** * Generates an order. * * Uses data submitted during class construction and performs all necessary * validations. If validations pass, creates the order. * * @since 7.0.0 * * @param string $user_action The user action, accepts `LLMS_Order_Generator::UA_COMMIT` or `LLMS_Order_Generator::UA_VALIDATE`. * @return WP_Error|LLMS_Order */ public function generate( $user_action = self::UA_COMMIT ) { $validate = $this->validate(); if ( is_wp_error( $validate ) ) { return $validate; } if ( self::UA_COMMIT === $user_action ) { $user = $this->commit_user(); if ( is_wp_error( $user ) ) { return $user; } } return $this->create(); } /** * Retrieves the coupon object for the order. * * @since 7.0.0 * * @return LLMS_Coupon|null */ public function get_coupon() { return $this->coupon; } /** * Retrieves the payment gateway instance for the order. * * @since 7.0.0 * * @return LLMS_Payment_Gateway|null */ public function get_gateway() { return $this->gateway; } /** * Retrieves the order id to use for the order. * * Attempts to locate an existing pending order by order key if it was submitted, * otherwise returns `new` which denotes a new order should be created. * * @since 7.0.0 * * @return integer|string */ protected function get_order_id() { $order_id = null; $key = $this->data['llms_order_key'] ?? null; $email = $this->data['email_address'] ?? null; $plan_id = $this->data['llms_plan_id'] ?? null; // Try to lookup using the order key if it was supplied. if ( $key ) { $order_id = $this->sanitize_retrieved_order_id( llms_get_order_by_key( $key, 'id' ) ); } // Try to lookup by user ID. if ( ! $order_id ) { $user_id = $this->find_user_id( $email ); $order_id = $user_id ? $this->sanitize_retrieved_order_id( llms_locate_order_for_user_and_plan( $user_id, $plan_id ) ) : null; } // Lookup by email address. if ( ! $order_id && $email ) { $order_id = $this->sanitize_retrieved_order_id( llms_locate_order_for_email_and_plan( $email, $plan_id ) ); } return $order_id ? $order_id : 'new'; } /** * Retrieves the access plan for the order. * * @since 7.0.0 * * @return LLMS_Access_Plan|null */ public function get_plan() { return $this->plan; } /** * Retrieves the order object. * * @since 7.0.0 * * @return LLMS_Order|null */ public function get_order() { return $this->order; } /** * Retrieves the student for the order. * * @since 7.0.0 * * @return LLMS_Student|null */ public function get_student() { return $this->student; } /** * Retrieves an array of data representing the student. * * The resulting array is intended to be used for setting up the `LLMS_Order` post's * user metadata, ideally passed to `LLMS_Order::init()`. * * @since 7.0.0 * * @return array */ public function get_user_data() { $map = array( 'billing_email' => 'email_address', 'billing_first_name' => 'first_name', 'billing_last_name' => 'last_name', 'billing_phone' => 'llms_phone', ); $data = array( 'billing_email' => '', 'billing_first_name' => '', 'billing_last_name' => '', 'billing_address_1' => '', 'billing_address_2' => '', 'billing_city' => '', 'billing_state' => '', 'billing_zip' => '', 'billing_country' => '', 'billing_phone' => '', ); foreach ( $data as $key => &$val ) { $data_key = $map[ $key ] ?? "llms_{$key}"; $val = $this->data[ $data_key ] ?? ''; } $data['user_id'] = $this->student ? $this->student->get( 'id' ) : ''; return $data; } /** * Sanitizes the order_id retrieved by {@see LLMS_Order_Generator::get_order_id()} to ensure it can be resumed or confirmed during checkout. * * Only orders with the `llms-pending` status can be resumed or confirmed. * * @since 7.0.0 * * @param null|int $order_id The order ID or `null` if the lookup didn't yield a result. * @return int|null Returns the submitted order ID if it's valid or `null`. */ private function sanitize_retrieved_order_id( $order_id ) { return $order_id && 'llms-pending' === get_post_status( $order_id ) ? $order_id : null; } /** * Performs all required data validations necessary to create the order. * * @since 7.0.0 * * @param boolean $validate_order Whether or not order data should be validated. This is `true` when running `confirm()` and `false` otherwise. * @return boolean|WP_Error Returns `true` if all validations pass or an error object. */ protected function validate( $validate_order = false ) { /** * Allows 3rd party validation prior to generation of an order. * * This validation hook runs prior to all default validation. * * @since 7.0.0 * * @param null|WP_Error $validation_error Halts checkout and returns the supplied error. */ $before_validation = apply_filters( 'llms_before_generate_order_validation', null ); if ( is_wp_error( $before_validation ) ) { return $before_validation; } $validations = array( 'validate_plan', 'validate_coupon', 'validate_gateway', 'validate_terms', 'validate_user', ); if ( $validate_order ) { array_unshift( $validations, 'validate_order' ); } foreach ( $validations as $func ) { $res = $this->{$func}(); if ( is_wp_error( $res ) ) { return $res; } } /** * Allows 3rd party validation prior to generation of an order. * * This validation hook runs after all default validation. * * @since 7.0.0 * * @param boolean|WP_Error $validation_error Halts checkout and returns the supplied error. */ return apply_filters( 'llms_after_generate_order_validation', true ); } /** * Validates the coupon. * * @since 7.0.0 * * @return boolean|WP_Error Returns `true` on success or an error object. */ protected function validate_coupon() { // If a coupon is being used, validate it. if ( ! empty( $this->data['llms_coupon_code'] ) ) { $code = sanitize_text_field( $this->data['llms_coupon_code'] ); // Locate the coupon post ID. $coupon_id = llms_find_coupon( $code ); if ( ! $coupon_id ) { return $this->error( self::E_COUPON_NOT_FOUND, sprintf( // Translators: %s = The user-submitted coupon code. __( 'Coupon code "%s" not found.', 'lifterlms' ), $code ) ); } // Validate the coupon for the current plan. $coupon = llms_get_post( $coupon_id ); $valid = $coupon->is_valid( $this->plan->get( 'id' ) ); if ( is_wp_error( $valid ) ) { return $this->error( self::E_COUPON_INVALID, $valid->get_error_message() ); } $this->coupon = $coupon; } return true; } /** * Validates the payment gateway. * * @since 7.0.0 * * @return boolean|WP_Error Returns `true` on success or an error object. */ protected function validate_gateway() { $coupon_id = $this->coupon ? $this->coupon->get( 'id' ) : null; /** * If payment is required, verify we have a gateway. * * For free plans the manual gateway is automatically used, whether or not it's enabled. */ if ( $this->plan->requires_payment( $coupon_id ) && empty( $this->data['llms_payment_gateway'] ) ) { return $this->error( self::E_GATEWAY_REQUIRED, __( 'No payment method selected.', 'lifterlms' ) ); } $gateway_id = $this->data['llms_payment_gateway'] ?? 'manual'; $is_valid = llms_can_gateway_be_used_for_plan( $gateway_id, $this->plan ); if ( is_wp_error( $is_valid ) ) { return $is_valid; } $this->gateway = llms()->payment_gateways()->get_gateway_by_id( $gateway_id ); return true; } /** * Validates the order. * * Ensures the submitted order key is valid and that the order can be confirmed. * * @since 7.0.0 * * @return boolean|WP_Error Returns `true` on success or an error object. */ protected function validate_order() { $order_id = $this->get_order_id(); if ( 'new' === $order_id || 'llms_order' !== get_post_type( $order_id ) ) { return $this->error( self::E_ORDER_NOT_FOUND, __( 'Could not locate an order to confirm.', 'lifterlms' ) ); } $order = llms_get_post( $order_id ); if ( ! $order->can_be_confirmed() ) { return $this->error( self::E_ORDER_NOT_CONFIRMABLE, __( 'Could not locate an order to confirm.', 'lifterlms' ) ); } $this->order = $order; return true; } /** * Validates the access plan. * * Ensures the access plan data was submitted and that it's a valid plan. * * @since 7.0.0 * * @return boolean|WP_Error Returns `true` on success or an error object. */ protected function validate_plan() { $plan_id = $this->data['llms_plan_id'] ?? null; if ( ! $plan_id ) { return $this->error( self::E_PLAN_REQUIRED, __( 'Missing access plan ID.', 'lifterlms' ) ); } $plan = llms_get_post( $plan_id ); if ( ! $plan || 'llms_access_plan' !== $plan->get( 'type' ) ) { return $this->error( self::E_PLAN_NOT_FOUND, __( 'Access plan not found.', 'lifterlms' ) ); } $this->plan = $plan; return true; } /** * Validates the site's terms and conditions were submitted. * * @since 7.0.0 * * @return boolean|WP_Error Returns `true` on success or an error object. */ protected function validate_terms() { if ( llms_are_terms_and_conditions_required() && ! llms_parse_bool( $this->data['llms_agree_to_terms'] ?? 'no' ) ) { return $this->error( self::E_SITE_TERMS, sprintf( // Translators: %s = The title of the site's LifterLMS Terms and Conditions page. __( 'You must agree to the %s.', 'lifterlms' ), get_the_title( get_option( 'lifterlms_terms_page_id' ) ) ) ); } return true; } /** * Validates the submitted user data. * * @since 7.0.0 * * @return boolean|WP_Error Returns `true` on success or an error object. */ protected function validate_user() { $validate = llms_validate_user( $this->data ); if ( is_wp_error( $validate ) ) { return $validate; } // If validation passes, determine if the user already exists and, if they do, validate their enrollment. $email = $this->data['email_address'] ?? null; $user = $email ? get_user_by( 'email', $email ) : false; if ( $user && llms_is_user_enrolled( $user->ID, $this->plan->get( 'product_id' ) ) ) { return $this->error( self::E_USER_ENROLLED, sprintf( // Translators: %s = The title of the course or membership. __( 'You already have access to %s.', 'lifterlms' ), get_the_title( $this->plan->get( 'product_id' ) ) ) ); } return true; } }
Expand full source code Collapse full source code View on GitHub
Methods Methods
- __construct — Constructor.
- commit_user — Registers or updates the user from the submitted data.
- confirm — Confirms an existing pending order.
- create — Creates a new pending order.
- error — Returns an error object.
- find_user_id — Attempts to locate a user ID.
- generate — Generates an order.
- get_coupon — Retrieves the coupon object for the order.
- get_gateway — Retrieves the payment gateway instance for the order.
- get_order — Retrieves the order object.
- get_order_id — Retrieves the order id to use for the order.
- get_plan — Retrieves the access plan for the order.
- get_student — Retrieves the student for the order.
- get_user_data — Retrieves an array of data representing the student.
- sanitize_retrieved_order_id — Sanitizes the order_id retrieved by {@see LLMS_Order_Generator::get_order_id()} to ensure it can be resumed or confirmed during checkout.
- validate — Performs all required data validations necessary to create the order.
- validate_coupon — Validates the coupon.
- validate_gateway — Validates the payment gateway.
- validate_order — Validates the order.
- validate_plan — Validates the access plan.
- validate_terms — Validates the site's terms and conditions were submitted.
- validate_user — Validates the submitted user data.
Changelog Changelog
Version | Description |
---|---|
7.0.0 | Introduced. |