LLMS_Order
LifterLMS order model
Description Description
Provides CRUD operations for the llms_order
post type.
Source Source
File: includes/models/model.llms.order.php
class LLMS_Order extends LLMS_Post_Model { /** * Database post type. * * @var string */ protected $db_post_type = 'llms_order'; /** * Model post type. * * @var string */ protected $model_post_type = 'order'; /** * Meta properties. * * @var array */ 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_ended' => 'yesno', '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_next_payment' => 'text', 'date_trial_end' => 'text', 'temp_gateway_ids' => 'array', ); /** * Add an admin-only note to the order visible on the admin panel * notes are recorded using the wp comments API & DB * * @since 3.0.0 * @since 3.35.0 Sanitize $_SERVER data. * * @param string $note Note content. * @param boolean $added_by_user Optional. If this is an admin-submitted note adds user info to note meta. Default is false. * @return null|int Null on error or WP_Comment ID of the note. */ 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.', '', sanitize_text_field( wp_unslash( $_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 * * @since 3.0.0 * * @return void */ 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 next payment due date * * @since 3.10.0 * @since 3.12.0 Unknown. * @since 3.37.6 Now uses the last successful transaction time to calculate from when the previously * stored next payment date is in the future. * @since 4.9.0 Fix comparison for PHP8 compat. * @since 5.3.0 Determine if a limited order has ended based on number of remaining payments in favor of current date/time. * * @param string $format PHP date format used to format the returned date string. * @return string The formatted next payment due date or an empty string when there is no next payment. */ private function calculate_next_payment_date( $format = 'Y-m-d H:i:s' ) { // If the limited plan has already ended return early. $remaining = $this->get_remaining_payments(); if ( 0 === $remaining ) { // This filter is documented below. return apply_filters( 'llms_order_calculate_next_payment_date', '', $format, $this ); } $start_time = $this->get_date( 'date', 'U' ); $next_payment_time = $this->get_date( 'date_next_payment', 'U' ); $last_txn_time = $this->get_last_transaction_date( 'llms-txn-succeeded', 'recurring', '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 { /** * Calculate next payment date from the saved `date_next_payment` calculated during * the previous recurring transaction or during order initialization. * * This condition will be encountered during the 2nd, 3rd, 4th, etc... recurring payments. */ if ( $next_payment_time && $next_payment_time < llms_current_time( 'timestamp' ) ) { $from_time = $next_payment_time; /** * Use the order's last successful transaction date. * * This will be encountered when any amount of "chaos" is * introduced causing the previously stored `date_next_payment` * to be GREATER than the current time. * * Orders created */ } elseif ( $last_txn_time && $last_txn_time > $start_time ) { $from_time = $last_txn_time; /** * Use the order's creation time. * * This condition will be encountered for the 1st recurring payment only. */ } else { $from_time = $start_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. */ $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++; } } /** * Filter the calculated next payment date * * @since 3.10.0 * * @param string $ret The formatted next payment due date or an empty string when there is no next payment. * @param string $format The requested date format. * @param LLMS_Order $order The order object. */ return apply_filters( 'llms_order_calculate_next_payment_date', date( $format, $next_payment_time ), $format, $this ); } /** * Calculate the end date of the trial * * @since 3.10.0 * * @param string $format Optional. Desired return format of the date. Defalt is 'Y-m-d H:i:s'. * @return string */ 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 ); } /** * Determines if an order can be confirmed. * * An order can be confirmed only when the order's status is pending. * * Additional requirements can be introduced via the filter `llms_order_can_be_confirmed`. * * @since 7.0.0 * * @return boolean */ public function can_be_confirmed() { /** * Determine if the order can be confirmed. * * @since 3.34.4 * * @param boolean $can_be_confirmed Whether or not the order can be confirmed. * @param LLMS_Order $order Order object. * @param string $gateway_id Payment gateway ID. */ return apply_filters( 'llms_order_can_be_confirmed', ( 'llms-pending' === $this->get( 'status' ) ), $this, $this->get( 'payment_gateway' ) ); } /** * Determine if the order can be retried for recurring payments * * @since 3.10.0 * @since 5.2.0 Use strict type comparison. * @since 5.2.1 Combine conditions that return `false`. * * @return boolean */ public function can_be_retried() { $can_retry = true; if ( // Only recurring orders can be retried. ! $this->is_recurring() || // Recurring rety feature is disabled. ! llms_parse_bool( get_option( 'lifterlms_recurring_payment_retry', 'yes' ) ) || // Only active & on-hold orders qualify for a retry. ! in_array( $this->get( 'status' ), array( 'llms-active', 'llms-on-hold' ), true ) ) { $can_retry = false; } else { // 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' ) ) { $can_retry = false; } } /** * Filters whether or not a recurring order can be retried * * @since 5.2.1 * * @param boolean $can_retry Whether or not the order can be retried. * @param LLMS_Order $order Order object. */ return apply_filters( 'llms_order_can_be_retried', $can_retry, $this ); } /** * Determines if the order can be resubscribed to. * * @since 3.19.0 * @since 5.2.0 Use strict type comparison. * * @return bool */ public function can_resubscribe() { $can_resubscribe = false; if ( $this->is_recurring() ) { /** * Filters the order statuses from which an order can be reactivated. * * @since 7.0.0 * * @param string[] $allowed_statuses The list of allowed order statuses. */ $allowed_statuses = apply_filters( 'llms_order_status_can_resubscribe_from', array( 'llms-on-hold', 'llms-pending', 'llms-pending-cancel', ) ); $can_resubscribe = in_array( $this->get( 'status' ), $allowed_statuses, true ); } /** * Determines whether or not a user can resubscribe to an inactive recurring payment order. * * @since 3.19.0 * * @param boolean $can_resubscribe Whether or not a user can resubscribe. * @param LLMS_Order $order The order object. */ return apply_filters( 'llms_order_can_resubscribe', $can_resubscribe, $this ); } /** * Determines if the order's payment source can be changed. * * @since 7.0.0 * * @return boolean */ public function can_switch_source() { $can_switch = 'llms-active' === $this->get( 'status' ) || $this->can_resubscribe(); /** * Filters whether or not the order's payment source can be changed. * * @since 7.0.0 * * @param boolean $can_switch Whether or not the order's source can be switched. * @param LLMS_Order $order The order object. */ return apply_filters( 'llms_order_can_switch_source', $can_switch, $this ); } /** * Generate an order key for the order * * @since 3.0.0 * * @return string */ public function generate_order_key() { /** * Modify the generated order key for the order. * * @since 3.0.0 * @since 5.2.1 Added the `$order` parameter. * * @param string $order_key The generated order key. * @param LLMS_Order $order_key Order object. */ return apply_filters( 'lifterlms_generate_order_key', uniqid( 'order-' ), $this ); } /** * Determine the date when access will expire * * Based on the access settings of the access plan * at the `$start_date` of access. * * @since 3.0.0 * @since 3.19.0 Unknown. * * @param string $format Optional. Date format. Default is 'Y-m-d'. * @return string Date string. * "Lifetime Access" for plans with lifetime access. * "To be Determined" for limited date when access hasn't started yet. */ 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. * * @since 3.0.0 * @since 3.19.0 Unknown. * @since 5.2.0 Use stric type comparison. * * @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. */ 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, true ) ) { 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 * * @since 3.19.0 * * @return array */ protected function get_action_args() { return array( 'order_id' => $this->get( 'id' ), ); } /** * Get the formatted coupon amount with a currency symbol or percentage * * @since 3.0.0 * * @param string $payment Coupon discount type, either 'regular' or 'trial'. * @return string */ 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 * * @since 3.0.0 * @since 3.18.0 Unknown. * * @return string */ 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' ) ); } /** * Retrieve the customer's full billing address * * @since 5.2.0 * * @return string */ public function get_customer_full_address() { $billing_address_1 = $this->get( 'billing_address_1' ); if ( empty( $billing_address_1 ) ) { return ''; } $address = array( trim( $billing_address_1 . ' ' . $this->get( 'billing_address_2' ) ), ); $address[] = trim( $this->get( 'billing_city' ) . ' ' . $this->get( 'billing_state' ) ); $address[] = $this->get( 'billing_zip' ); $address[] = llms_get_country_name( $this->get( 'billing_country' ) ); return implode( ', ', array_filter( $address ) ); } /** * An array of default arguments to pass to $this->create() when creating a new post * * @since 3.0.0 * @since 3.10.0 Unknown. * @since 5.3.1 Set the `post_date` property using `llms_current_time()`. * @since 5.9.0 Remove usage of deprecated `strftime()`. * * @param string $title Title to create the post with. * @return array */ protected function get_creation_args( $title = '' ) { $date = llms_current_time( 'mysql' ); if ( empty( $title ) ) { $title = sprintf( // Translators: %1$s = Transaction creation date. __( 'Order – %1$s', 'lifterlms' ), date_format( date_create( $date ), 'M d, Y @ h:i A' ) ); } return apply_filters( "llms_{$this->model_post_type}_get_creation_args", array( 'comment_status' => 'closed', 'ping_status' => 'closed', 'post_author' => 1, 'post_content' => '', 'post_date' => $date, '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 * * @since 1.0.0 * * @return LLMS_Payment_Gateway|WP_Error Instance of the LLMS_Payment_Gateway extending class used for the payment. * WP_Error if the gateway cannot be located, e.g. because it's no longer enabled. */ 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. * * @since 3.0.0 * * @return mixed */ 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. * * @since 3.0.0 * * @param integer $number Number of comments to return. * @param integer $page Page number for pagination. * @return array */ 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 * * @since 3.8.0 * * @return LLMS_Post_Model|WP_Post|null|false LLMS_Post_Model extended object (LLMS_Course|LLMS_Membership), * null if WP get_post() fails, * false if LLMS_Post_Model extended class isn't found. */ public function get_product() { return llms_get_post( $this->get( 'product_id' ) ); } /** * Retrieve the last (most recent) transaction processed for the order. * * @since 3.0.0 * @since 7.1.0 Skip counting the total rows found when retrieving the last transaction. * * @param array|string $status Filter by status (see transaction statuses). By default looks for any status. * @param array|string $type Filter by type [recurring|single|trial]. By default looks for any type. * @return LLMS_Transaction|false instance of the LLMS_Transaction or false if none found */ public function get_last_transaction( $status = 'any', $type = 'any' ) { $txns = $this->get_transactions( array( 'per_page' => 1, 'status' => $status, 'type' => $type, 'no_found_rows' => true, ) ); if ( $txns['count'] ) { return array_pop( $txns['transactions'] ); } return false; } /** * Retrieve the date of the last (most recent) transaction * * @since 3.0.0 * * @param array|string $status Optional. Filter by status (see transaction statuses). Default is 'llms-txn-succeeded'. * @param array|string $type Optional. Filter by type [recurring|single|trial]. By default looks for any type. * @param string $format Optional. Date format of the return. Default is 'Y-m-d H:i:s'. * @return string|false Date or false if none found. */ 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 * * @since 3.0.0 * @since 3.19.0 Unknown. * @since 5.2.0 Use stric type comparisons. * * @param string $format Optional. Date return format. Default is 'Y-m-d H:i:s'. * @return string */ 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' ), true ) ) { return new WP_Error( 'invalid-status', __( 'Invalid order status', 'lifterlms' ), $this->get( 'status' ) ); } // Retrieve the saved due date. $next_payment_time = $this->get_date( 'date_next_payment', 'U' ); // Calculate it if not saved. if ( ! $next_payment_time ) { $next_payment_time = $this->calculate_next_payment_date( 'U' ); if ( ! $next_payment_time ) { return new WP_Error( 'plan-ended', __( 'No more payments due', 'lifterlms' ) ); } } /** * Filter the next payment due date. * * A timestamp should always be returned as the conversion to the requested format * will be performed on the returned value. * * @since 3.0.0 * * @param int $next_payment_time Unix timestamp for the next payment due date. * @param LLMS_Order $order Order object. * @param string $format Requested date format. */ $next_payment_time = apply_filters( 'llms_order_get_next_payment_due_date', $next_payment_time, $this, $format ); return date_i18n( $format, $next_payment_time ); } /** * Retrieve the timestamp of the next scheduled event for a given action * * @since 4.6.0 * * @param string $action Action hook ID. Core actions are "llms_charge_recurring_payment", "llms_access_plan_expiration". * @return int|false Returns the timestamp of the next action as an integer or `false` when no action exist. */ public function get_next_scheduled_action_time( $action ) { return as_next_scheduled_action( $action, $this->get_action_args() ); } /** * Retrieves the number of payments remaining for a recurring plan with a limited number of payments * * @since 5.3.0 * * @return bool|int Returns `false` for invalid order types (single-payment orders or recurring orders * without a billing length). Otherwise returns the number of remaining payments as an integer. */ public function get_remaining_payments() { $remaining = false; if ( $this->has_plan_expiration() ) { $len = $this->get( 'billing_length' ); $txns = $this->get_transactions( array( 'status' => array( 'llms-txn-succeeded', 'llms-txn-refunded' ), 'per_page' => 1, 'type' => array( 'recurring', 'single' ), // If a manual payment is recorded it's counted a single payment and that should count. ) ); $remaining = $len - $txns['total']; } /** * Filters the number of payments remaining for a recurring plan with a limited number of payments. * * @since 5.3.0 * * @param bool|int $remaining Number of remaining payments or `false` when called against invalid order types. * @param LLMS_Order $order Order object. */ return apply_filters( 'llms_order_remaining_payments', $remaining, $this ); } /** * Get configured payment retry rules * * @since 3.10.0 * * @return array[] { * An array of retry rule arrays. * * @type int $delay The number of seconds to delay to use when scheduling the retry attempt. * @type string $status The status of the order while awaiting the next retry. * @type bool $notifications Whether or not to trigger notifications to the student/user. * } */ 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, ), ); /** * Filters the automatic payment recurring retry rules. * * @since 7.0.0 * * @param array $rules Array of retry rule arrays {@see LLMS_Order::get_retry_rules()}. * @param LLMS_Order $rules The order object. */ return apply_filters( 'llms_order_automatic_retry_rules', $rules, $this ); } /** * SQL query to retrieve total amounts for transactions by type * * @since 3.0.0 * @since 3.35.0 Prepare SQL query properly. * * @param string $type Optional. Type can be 'amount' or 'refund_amount'. Default is 'amount'. * @return float */ 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; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $post_statuses is prepared above. $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 = %s AND m1.meta_value = %d AND m2.meta_key = %s ;", array( "{$this->meta_prefix}order_id", $this->get( 'id' ), "{$this->meta_prefix}{$type}", ) ) ); // db call ok; no-cache ok. // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared 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. * * @since 3.0.0 * @since 7.1.0 Skip counting the total rows found when retrieving the first transaction. * * @param string $format Desired return format of the date. * @return string */ 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', 'no_found_rows' => true, ) ); if ( $txns['count'] ) { $txn = array_pop( $txns['transactions'] ); $date = $txn->get_date( 'date', $format ); } else { $date = $this->get_date( 'date', $format ); } /** * Filter the order start date. * * @since 3.0.0 * @since 7.1.0 Added the `$format` parameter. * * @param string $date The formatted start date for the order. * @param LLMS_Order $order The order object. * @param string $format The requested format of the date. */ return apply_filters( 'llms_order_get_start_date', $date, $this, $format ); } /** * Retrieves the user action required when changing the order's payment source. * * @since 7.0.0 * * @return null|string Returns `switch` when the payment source can be switched and `pay` when payment on the new source * is required before switching. A `null` return indicates that the order's payment source cannot be switched. */ public function get_switch_source_action() { $action = null; if ( $this->can_switch_source() ) { $action = in_array( $this->get( 'status' ), array( 'llms-active', 'llms-pending-cancel' ), true ) ? 'switch' : 'pay'; } /** * Filters the required user action for the order when switching the order's payment source. * * @since 7.0.0 * * @param null|string $action The switch action ID or `null` when the payment source cannot be switched. * @param LLMS_Order $order The order object. */ return apply_filters( 'llms_order_switch_source_action', $action, $this ); } /** * Retrieve an array of transactions associated with the order according to supplied arguments. * * @since 3.0.0 * @since 3.10.0 Unknown. * @since 3.37.6 Add additional return property, `total`, which returns the total number of found transactions. * @since 5.2.0 Use stric type comparisons. * @since 7.1.0 Added `no_found_rows` parameter. * * @param array $args { * Hash of query argument data, ultimately passed to a WP_Query. * * @type string|string[] $status Transaction post status or array of transaction post status. Defaults to "any". * @type string|string[] $type Transaction types or array of transaction types. Defaults to "any". * Accepts "recurring", "single", or "trial". * @type int $per_page Number of transactions to include in the return. Default `50`. * @type int $paged Result set page number. * @type string $order Result set order. Default "DESC". Accepts "DESC" or "ASC". * @type string $orderby Result set ordering field. Default "date". * @type bool $no_found_rows Whether to skip counting the total rows found. Enabling can improve * performance. Default `false`. * } * @return array */ 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. 'no_found_rows' => false, ) ) ); // 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, true ) ) { $statuses = array( $status ); } elseif ( is_array( $status ) ) { $temp = array(); foreach ( $status as $stat ) { if ( in_array( (string) $stat, $statuses, true ) ) { $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( /** * Filters the order's transactions query aguments. * * @since 3.0.0 * @since 7.1.0 Added `$no_found_rows` arg. * * @param array $query_args { * Hash of query argument data passed to a WP_Query. * * @type string|string[] $status Transaction post status or array of transaction post status. * Defaults to "any". * @type string|string[] $type Transaction types or array of transaction types. * Defaults to "any". * Accepts "recurring", "single", or "trial". * @type int $per_page Number of transactions to include in the return. Default `50`. * @type int $paged Result set page number. * @type string $order Result set order. Default "DESC". Accepts "DESC" or "ASC". * @type string $orderby Result set ordering field. Default "date". * @type bool $no_found_rows Whether to skip counting the total rows found. * Enabling can improve performance. Default false. * } */ 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, 'no_found_rows' => $no_found_rows, ) ), $this, $status ); $transactions = array(); foreach ( $query->posts as $post ) { $transactions[ $post->ID ] = llms_get_post( $post ); } return array( 'total' => $query->found_posts, 'count' => $query->post_count, 'page' => $paged, 'pages' => $query->max_num_pages, 'transactions' => $transactions, ); } /** * Retrieve the date when a trial will end * * @since 3.0.0 * * @param string $format Optional. Date return format. Default is 'Y-m-d H:i:s'. * @return string */ 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 * * @since 3.0.0 * @since 3.1.3 Handle legacy orders. * * @param string $type Optional. Revenue type [grosse|net]. Default is 'net'. * @return float */ 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 * * @since 3.0.0 * @since 3.8.0 Unknown. * * @return string */ 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 * * @since 3.0.0 * * @return boolean */ public function has_access() { return ( 'active' === $this->get_access_status() ) ? true : false; } /** * Determine if a coupon was used * * @since 3.0.0 * * @return boolean */ 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 * * @since 3.0.0 * * @return boolean */ public function has_discount() { return ( $this->has_coupon() || $this->has_sale() ); } /** * Determine if a recurring order has a limited number of payments * * @since 5.3.0 * * @return boolean Returns `true` for recurring orders with a billing length and `false` otherwise. */ public function has_plan_expiration() { return ( $this->is_recurring() && ( $this->get( 'billing_length' ) > 0 ) ); } /** * Determine if the access plan was on sale during the purchase * * @since 3.0.0 * * @return boolean */ public function has_sale() { return ( 'yes' === $this->get( 'on_sale' ) ); } /** * Determine if there's a payment scheduled for the order * * @since 3.0.0 * * @return boolean */ 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 * * @since 3.0.0 * * @return boolean True if has a trial, false if it doesn't. */ public function has_trial() { return ( $this->is_recurring() && 'yes' === $this->get( 'trial_offer' ) ); } /** * Determine if the trial period has ended for the order * * @since 3.0.0 * @since 3.10.0 Unknown. * * @return boolean True if ended, false if not ended. */ public function has_trial_ended() { return ( llms_current_time( 'timestamp' ) >= $this->get_trial_end_date( 'U' ) ); } /** * Initializes a new order with user, plan, gateway, and coupon metadata. * * Assumes all data passed in has already been validated. * * @since 3.8.0 * @since 3.10.0 Unknown. * @since 5.3.0 Don't set unused legacy property `date_billing_end`. * @since 7.0.0 Use `LLMS_Order::set_user_data()` to update user data. * * @param array|LLMS_Student|WP_User|integer $user_data User info for the person placing the order. See * {@see LLMS_Order::set_user_data()} for more info. * @param LLMS_Access_Plan $plan The purchase access plan. * @param LLMS_Payment_Gateway $gateway Gateway being used. * @param LLMS_Coupon $coupon Coupon object or `false` if no coupon used. * @return LLMS_Order */ public function init( $user_data, $plan, $gateway, $coupon = false ) { $this->set_user_data( $user_data ); // 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' ); $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' ) ); } /** * Action triggered after the order is initialized. * * @since Unknown. * @since 7.0.0 Added `$user_data` parameter. * The `$student` parameter returns an "empty" student object * if the method's input data is an array instead of an existing * user object. * * @param LLMS_Order $order The order object. * @param LLMS_Student $student The student object. If an array of data is passed * to `LLMS_Order::init()` then an empty student object * will be passed. * @param array|LLMS_Student|WP_User|integer $user_data User data. */ do_action( 'lifterlms_new_pending_order', $this, is_array( $user_data ) ? new LLMS_Student( null, false ) : llms_get_student( $user_data ), $user_data ); return $this; } /** * Determine if the order is a legacy order migrated from 2.x * * @since 3.0.0 * * @return boolean */ public function is_legacy() { return ( 'publish' === $this->get( 'status' ) ); } /** * Determine if the order is recurring or singular * * @since 3.0.0 * * @return boolean True if recurring, false if not. */ 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. * @since 4.7.0 Add `plan_ended` metadata when a plan ends. * @since 5.2.0 Move scheduling recurring payment into a proper method. * * @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(); // Unschedule and reschedule. if ( $date && ! is_wp_error( $date ) ) { $this->schedule_recurring_payment( $date ); } 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' ) ); $this->set( 'plan_ended', 'yes' ); } } } /** * Handles scheduling recurring payment retries when the gateway supports them * * @since 3.10.0 * @since 7.0.0 Added return value. * * @return null|boolean Returns `null` if the order cannot be retried, `false` when all retry rules have been tried (or none exist), and `true` * when a retry is scheduled. */ public function maybe_schedule_retry() { if ( ! $this->can_be_retried() ) { return null; } // Get the index of the rule to use for this retry. $current_rule_index = $this->get( 'last_retry_rule' ); if ( '' === $current_rule_index ) { $current_rule_index = 0; } else { ++$current_rule_index; } $rules = $this->get_retry_rules(); $current_rule = $rules[ $current_rule_index ] ?? false; // No rule to run. if ( ! $current_rule ) { $this->set_status( 'failed' ); $this->set( 'last_retry_rule', '' ); $this->add_note( esc_html__( 'Maximum retry attempts reached.', 'lifterlms' ) ); /** * Action triggered when there are not more recurring payment retry rules. * * @since 3.10.0 * * @param LLMS_Order $order The order object. */ do_action( 'llms_automatic_payment_maximum_retries_reached', $this ); return false; } $timestamp = current_time( 'timestamp' ) + $current_rule['delay']; $this->set_date( 'next_payment', date_i18n( 'Y-m-d H:i:s', $timestamp ) ); $this->set_status( $current_rule['status'] ); $this->set( 'last_retry_rule', $current_rule_index ); $this->add_note( sprintf( // Translators: %s = next attempt date. esc_html__( 'Automatic retry attempt scheduled for %s', 'lifterlms' ), date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp ) ) ); // If notifications should be sent, trigger them. if ( $current_rule['notifications'] ) { /** * Triggers the "Payment Retry Scheduled" notification. * * @since 3.10.0 * * @param LLMS_Order $order The order object. */ do_action( 'llms_send_automatic_payment_retry_notification', $this ); } /** * Action triggered after a recurring payment retry is successfully scheduled. * * @since 3.10.0 * * @param LLMS_Order $order The order object. */ do_action( 'llms_automatic_payment_retry_scheduled', $this ); return true; } /** * Record a transaction for the order * * @since 3.0.0 * * @param array $data Optional array of additional data to store for the transaction. * @return LLMS_Transaction Instance of LLMS_Transaction for the created transaction. */ 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. * * @since 3.10.0 * @since 3.19.0 Unknown. * * @param string $date_key Date field to set. * @param string $date_val Date string or a unix time stamp. */ 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 * * @since 3.8.0 * @since 3.10.0 Unknown. * @since 5.2.0 Prefer `array_key_exists( $key, $keys )` over `in_array( $key, array_keys( $assoc_array ) )`. * * @param string $status Status name, accepts unprefixed statuses. * @return void */ public function set_status( $status ) { if ( false === strpos( $status, 'llms-' ) ) { $status = 'llms-' . $status; } if ( array_key_exists( $status, llms_get_order_statuses( $this->get( 'order_type' ) ) ) ) { $this->set( 'status', $status ); } } /** * Sets user-related metadata for the order. * * @since 7.0.0 * * @param array|LLMS_Student|WP_User|integer $user_or_data Accepts a raw array user meta-data or * an input string accepted by `llms_get_student()`. * When passing an existing user the data will be pulled * from the user metadata and saved to the order. * @return array { * Returns an associative array representing the user metadata that was stored on the order. * * @type integer $user_id User's WP_User id. * @type string $user_ip_address User's ip address. * @type string $billing_email User's email. * @type string $billing_first_name User's first name. * @type string $billing_last_name User's last name. * @type string $billing_address_1 User's address line 1. * @type string $billing_address_2 User's address line 2. * @type string $billing_city User's city. * @type string $billing_state User's state. * @type string $billing_zip User's zip. * @type string $billing_country User's country. * @type string $billing_phone User's phone. * } */ public function set_user_data( $user_or_data ) { $to_set = array( 'user_id' => '', 'billing_email' => '', 'billing_first_name' => '', 'billing_last_name' => '', 'billing_address_1' => '', 'billing_address_2' => '', 'billing_city' => '', 'billing_state' => '', 'billing_zip' => '', 'billing_country' => '', 'billing_phone' => '', ); $user = ! is_array( $user_or_data ) ? llms_get_student( $user_or_data ) : false; if ( $user ) { $user_or_data = array(); $map = array( 'user_id' => 'id', 'billing_email' => 'user_email', 'billing_phone' => 'phone', 'billing_first_name' => 'first_name', 'billing_last_name' => 'last_name', ); foreach ( array_keys( $to_set ) as $order_key ) { $to_set[ $order_key ] = $user->get( $map[ $order_key ] ?? $order_key ); } } // Only use the default IP address if it wasn't specified in the input array. $to_set['user_ip_address'] = $user_or_data['user_ip_address'] ?? llms_get_ip_address(); // Merge the data and remove excess keys. $to_set = array_intersect_key( array_merge( $to_set, $user_or_data ), $to_set ); $this->set_bulk( $to_set ); return $to_set; } /** * Record the start date of the access plan and schedule expiration if expiration is required in the future * * @since 3.0.0 * @since 3.19.0 Unknown. * @since 5.2.0 Use strict type comparision. * * @return void */ 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' ), true ) ) { $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. * @since 4.6.0 Use `$this->get_next_scheduled_action_time()` to determine if the action is currently scheduled. * * @return void */ public function unschedule_expiration() { if ( $this->get_next_scheduled_action_time( 'llms_access_plan_expiration' ) ) { 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. * @since 4.6.0 Use `$this->get_next_scheduled_action_time()` to determine if the action is currently scheduled. * * @return void */ public function unschedule_recurring_payment() { if ( $this->get_next_scheduled_action_time( 'llms_charge_recurring_payment' ) ) { $action_args = $this->get_action_args(); as_unschedule_action( 'llms_charge_recurring_payment', $action_args ); /** * Fired after a recurring payment is unscheduled * * @since 5.2.0 * * @param LLMS_Order $order LLMS_Order instance. * @param int $date Timestamp of the recurring payment date UTC. * @param array $action_args Arguments passed to the scheduler. */ do_action( 'llms_charge_recurring_payment_unscheduled', $this, $action_args ); } } /** * Schedule recurring payment * * It will unschedule the next recurring payment action, if any, before scheduling. * * @since 5.2.0 * * @param string $next_payment_date Optional. Next payment date. If not provided it'll be retrieved using `$this->get_next_payment_due_date()`. * @param boolean $gmt Optional. Whether the provided `$next_payment_date` date is gmt. Default is `false`. * Only applies when the `$next_payment_date` is provided. * @return WP_Error|integer WP_Error if the plan ended. Otherwise returns the return value of `as_schedule_single_action`: the action's ID. */ public function schedule_recurring_payment( $next_payment_date = false, $gmt = false ) { // Unschedule the next action (does nothing if no action scheduled). $this->unschedule_recurring_payment(); $date = $this->get_recurring_payment_due_date_for_scheduler( $next_payment_date, $gmt ); if ( is_wp_error( $date ) ) { return $date; } $action_args = $this->get_action_args(); // Schedule the payment. $action_id = as_schedule_single_action( $date, 'llms_charge_recurring_payment', $action_args ); /** * Fired after a recurring payment is scheduled * * @since 5.2.0 * * @param LLMS_Order $order LLMS_Order instance. * @param integer $date Timestamp of the recurring payment date UTC. * @param array $action_args Arguments passed to the scheduler. * @param integer $action_id Scheduled action ID. */ do_action( 'llms_charge_recurring_payment_scheduled', $this, $date, $action_args, $action_id ); return $action_id; } /** * Returns the recurring payment due date in a suitable format for the scheduler. * * @since 5.2.0 *
Expand full source code Collapse full source code View on GitHub
Properties Properties
The following post and post meta properties are accessible for this class. See LLMS_Post_Model::get() and LLMS_Post_Model::set() for more information.
- $access_expiration
-
(string) Access expiration type, accepts: lifetime (default), limited-period, or limited-date.
- $access_expires
-
(string) Date on which access expires in
m/d/Y
format. Only applicable when the$access_expiration
property is set to "limited-date". - $access_length
-
(int) Length of access from time of purchase, combine with the
$access_period
. Only applicable when the$access_expiration
property is set to "limited-period". - $access_period
-
(string) Time period of access from time of purchase, combine with
$access_length
. Only applicable when the$access_expiration
property is set to "limited-period". Accepts: year, month, week, or day. - $anonymized
-
(string) Determines if the order has been anonymized due to a personal information erasure request. Accepts "yes" or "no".
- $billing_address_1
-
(string) Customer billing address line 1.
- $billing_address_2
-
(string) Customer billing address line 2.
- $billing_city
-
(string) Customer billing city.
- $billing_country
-
(string) Customer billing country, two character ISO code.
- $billing_email
-
(string) Customer email address.
- $billing_first_name
-
(string) Customer first name.
- $billing_last_name
-
(string) Customer last name.
- $billing_phone
-
(string) Customer phone number.
- $billing_state
-
(string) Customer billing state.
- $billing_zip
-
(string) Customer billing zip/postal code.
- $billing_frequency
-
(int) The billing frequency interval. A value of
0
indicates a one-time payment. Accepts integers <= 6. - $billing_length
-
(int) Number of intervals to run payment for, combine with
$billing_period
&$billing_frequency
. A value of0
indicates that recurring payments run indefinitely (until cancelled). Only applicable if$billing_frequency
is not 0. - $billing_period
-
(string) The billing period. Combine with
$length
. Only applicable if$billing_frequency
is not 0. Accepts: year, month, week, or day. - $coupon_amount
-
(float) Amount of the coupon (flat/percentage) in relation to the plan amount.
- $coupon_amout_trial
-
(float) Amount of the coupon (flat/percentage) in relation to the plan trial amount where applicable.
- $coupon_code
-
(string) Coupon code applied to the order.
- $coupon_id
-
(int) The WP_Post ID of the used coupon.
- $coupon_type
-
(string) Type of coupon used, either percent or dollar.
- $coupon_used
-
(string) Whether or not a coupon was used for the order. Accepts yes or no.
- $coupon_value
-
(float) Value of the coupon. When on sale,
$sale_price
minus$total
; when not on sale$original_total
minus$total
. - $coupon_value_trial
-
(float) Value of the coupon applied to the trial. The
$trial_original_total
minus$trial_total
. - $currency
-
(string) Transaction's currency code.
- $date_access_expires
-
(string) Date when access should expire as a datetime string:
Y-m-d H:i:s
. - $date_next_payment
-
(string) Date when the next recurring payment is due as a datemtime string:
Y-m-d H:i:s
. Use function LLMS_Order::get_next_payment_due_date() instead of accessing directly! - $date_trial_end
-
(string) Date when the trial ends for orders with a trial as a datemtime string:
Y-m-d H:i:s
. Use function LLMS_Order::get_trial_end_date() instead of accessing directly! - $gateway_api_mode
-
(string) API Mode of the gateway when the transaction was made, either "test" or "live".
- $gateway_customer_id
-
(string) Gateway's unique ID for the customer who placed the order (if supported by the gateway).
- $gateway_source_id
-
(string) Gateway's unique ID for the card or source to be used for recurring subscriptions (if supported by gateway).
- $gateway_subscription_id
-
(string) Gateway's unique ID for the recurring subscription (if supported by the gateway).
- $id
-
(int) The WP_Post ID of the order.
- $last_retry_rule
-
(int) Rule number for current retry step for the order.
- $on_sale
-
(string) Whether or not sale pricing was used for the plan, either "yes" or "no".
- $order_key
-
(string) A unique identifier for the order that can be passed safely in URLs.
- $order_type
-
(string) Single or recurring order, either "single" or "recurring".
- $original_total
-
(float) Price of the order before applicable sale and coupon adjustments.
- $payment_gateway
-
(string) LifterLMS Payment Gateway ID (eg "paypal" or "stripe").
- $plan_id
-
(int) WP_Post ID of the purchased access plan.
- $plan_sku
-
(string) SKU of the purchased access plan.
- $plan_title
-
(string) Title / Name of the purchased access plan.
- $plan_ended
-
(string) Whether or not the payment plan has ended. Only applicable when the plan is not "unlimited". Accepts "yes" or "no".
- $product_id
-
(int) WP_Post ID of the purchased course or membership product.
- $product_sku
-
(string) SKU of the purchased product.
- $product_title
-
(string) Title / Name of the purchased product.
- $product_type
-
(string) Type of product purchased (course or membership).
- $sale_price
-
(float) Sale price before coupon adjustments.
- $sale_value
-
(float) The value of the sale,
$original_total
-$sale_price
. - $start_date
-
(string) Date when access was initially granted; this is used to determine when access expires.
- $temp_gateway_ids
-
(array) { An associative array containing gateway ids. The gateway IDs are cached in this meta property while the source is being switched. Any gateway running actions when a source is switched may need to know the previous source IDs which might be cleared or overwritten by other gateways during the switch. @type string customer The value of the
gateway_customer_id
property when the source switch starts. @type string source The value of thegateway_source_id
property when the source switch starts. @type string subscription The value of thegateway_subscription_id
property when the source switch starts. } - $total
-
(float) Actual price of the order, after applicable sale & coupon adjustments.
- $trial_length
-
(int) Length of the trial. Combined with $trial_period to determine the actual length of the trial.
- $trial_offer
-
(string) Whether or not there was a trial offer applied to the order, either yes or no.
- $trial_original_total
-
(float) Total price of the trial before applicable coupon adjustments.
- $trial_period
-
(string) Period for the trial period. Accepts: year, month, week, or day.
- $trial_total
-
(float) Total price of the trial after applicable coupon adjustments/
- $user_id
-
(int) Customer WP_User ID.
- $user_ip_address
-
(string) Customer's IP address at time of purchase.
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_confirmed — Determines if an order can be confirmed.
- can_be_retried — Determine if the order can be retried for recurring payments
- can_resubscribe — Determines if the order can be resubscribed to.
- can_switch_source — Determines if the order's payment source can be changed.
- generate_order_key — Generate an order key for the order
- get_access_expiration_date — Determine the date when access will expire
- get_access_status — Get the current status of a student's access
- 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_full_address — Retrieve the customer's full billing address
- 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
- 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_next_scheduled_action_time — Retrieve the timestamp of the next scheduled event for a given action
- get_notes — Get an array of the order notes
- get_product — Retrieve an LLMS_Post_Model object for the associated product
- get_recurring_payment_due_date_for_scheduler — Returns the recurring payment due date in a suitable format for the scheduler.
- get_remaining_payments — Retrieves the number of payments remaining for a recurring plan with a limited number of payments
- 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
- get_switch_source_action — Retrieves the user action required when changing the order's payment source.
- 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_plan_expiration — Determine if a recurring order has a limited number of payments
- 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 — Initializes a new order with user, plan, gateway, and coupon metadata.
- 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
- schedule_recurring_payment — Schedule recurring payment
- set_date — Date field setter for date fields that require things to be updated when their value changes
- set_status — Update the status of an order
- set_user_data — Sets user-related metadata for the order.
- start_access — Record the start date of the access plan and schedule expiration if expiration is required in the future
- supports_modify_recurring_payments — Determine whether the recurring payment for this order can be modified.
- unschedule_expiration — Cancels a scheduled expiration action
- unschedule_recurring_payment — Cancels a scheduled recurring payment action
Changelog Changelog
Version | Description |
---|---|
5.3.0 | Removed usage of the meta property date_billing_end and removed private method calculate_billing_end_date() . |
4.7.0 | Added plan_ended meta property. |
3.35.0 | Prepare transaction revenue SQL query properly; Sanitize $_SERVER data. |
3.32.0 | Update to use latest action-scheduler functions. |
3.0.0 | Introduced. |