find_by_code( $_id_or_code_or_name ); // Name } elseif ( $by_name ) { $discount = $this->find_by_name( $_id_or_code_or_name ); // Default to ID } else { $discount = edd_get_discount( absint( $_id_or_code_or_name ) ); } // Setup or bail if ( ! empty( $discount ) ) { $this->setup_discount( $discount ); } else { return false; } } /** * Magic __get method to dispatch a call to retrieve a protected property. * * @since 2.7 * * @param mixed $key * @return mixed */ public function __get( $key = '' ) { $key = sanitize_key( $key ); // Back compat for ID if ( 'discount_id' === $key || 'ID' === $key ) { return (int) $this->id; // Method } elseif ( method_exists( $this, "get_{$key}" ) ) { return call_user_func( array( $this, "get_{$key}" ) ); // Property } elseif ( property_exists( $this, $key ) ) { return $this->{$key}; // Other... } else { // Account for old property keys from pre 3.0 switch ( $key ) { case 'post_author': break; case 'post_date': case 'post_date_gmt': return $this->date_created; case 'post_modified': case 'post_modified_gmt': return $this->date_modified; case 'post_content': case 'post_title': return $this->name; case 'post_excerpt': case 'post_status': return $this->status; case 'comment_status': case 'ping_status': case 'post_password': case 'post_name': case 'to_ping': case 'pinged': case 'post_modified': case 'post_modified_gmt': case 'post_content_filtered': case 'post_parent': case 'guid': case 'menu_order': case 'post_mime_type': case 'comment_count': case 'filter': return ''; case 'post_type': return 'edd_discount'; case 'expiration': return $this->get_expiration(); case 'start': return $this->start_date; case 'min_price': return $this->min_charge_amount; case 'use_once': case 'is_single_use': case 'once_per_customer': return $this->get_is_single_use(); case 'uses': return $this->use_count; case 'not_global': case 'is_not_global': return 'global' === $this->scope ? false : true; } return new WP_Error( 'edd-discount-invalid-property', sprintf( __( 'Can\'t get property %s', 'easy-digital-downloads' ), $key ) ); } } /** * Magic __set method to dispatch a call to update a protected property. * * @since 2.7 * * @see set() * * @param string $key Property name. * @param mixed $value Property value. * * @return mixed Value of setter being dispatched to. */ public function __set( $key, $value ) { $key = sanitize_key( $key ); // Only real properties can be saved. $keys = array_keys( get_class_vars( get_called_class() ) ); $old_keys = array( 'is_single_use', 'uses', 'expiration', 'start', 'min_price', 'use_once', 'is_not_global', ); if ( ! in_array( $key, $keys, true ) && ! in_array( $key, $old_keys, true ) ) { return false; } // Dispatch to setter method if value needs to be sanitized if ( method_exists( $this, 'set_' . $key ) ) { return call_user_func( array( $this, 'set_' . $key ), $key, $value ); } elseif ( in_array( $key, $old_keys, true ) ) { switch ( $key ) { case 'expiration': $this->end_date = $value; break; case 'start': $this->start_date = $value; break; case 'min_price': $this->min_charge_amount = $value; break; case 'use_once': case 'is_single_use': $this->once_per_customer = $value; break; case 'uses': $this->use_count = $value; break; case 'not_global': case 'is_not_global': $this->scope = $value ? 'not_global' : 'global'; break; } } else { $this->{$key} = $value; } } /** * Handle method dispatch dynamically. * * @param string $method Method name. * @param array $args Arguments to be passed to method. * * @return mixed */ public function __call( $method, $args ) { $property = strtolower( str_replace( array( 'setup_', 'get_' ), '', $method ) ); if ( ! method_exists( $this, $method ) && property_exists( $this, $property ) ) { return $this->{$property}; } } /** * Magic __toString method. * * @since 3.0 */ public function __toString() { return $this->code; } /** * Converts the instance of the EDD_Discount object into an array for special cases. * * @since 2.7 * * @return array EDD_Discount object as an array. */ public function array_convert() { return get_object_vars( $this ); } /** * Find a discount in the database with the code supplied. * * @since 2.7 * @access private * * @param string $code Discount code. * @return object WP_Post instance of the discount. */ private function find_by_code( $code = '' ) { return edd_get_discount_by( 'code', $code ); } /** * Find a discount in the database with the name supplied. * * @since 2.7 * @access private * * @param string $name Discount name. * @return object WP_Post instance of the discount. */ private function find_by_name( $name = '' ) { return edd_get_discount_by( 'name', $name ); } /** * Setup object vars with discount WP_Post object. * * @since 2.7 * @access private * * @param object $discount WP_Post instance of the discount. * @return bool Object initialization successful or not. */ private function setup_discount( $discount = null ) { if ( is_null( $discount ) ) { return false; } if ( ! is_object( $discount ) ) { return false; } if ( is_wp_error( $discount ) ) { return false; } /** * Fires before the instance of the EDD_Discount object is set up. * * @since 2.7 * * @param object EDD_Discount EDD_Discount instance of the discount object. * @param object WP_Post $discount WP_Post instance of the discount object. */ do_action( 'edd_pre_setup_discount', $this, $discount ); $vars = get_object_vars( $discount ); foreach ( $vars as $key => $value ) { switch ( $key ) { case 'start_date': case 'end_date': if ( '0000-00-00 00:00:00' === $value || is_null( $value ) ) { $this->{$key} = false; break; } case 'notes': if ( ! empty( $value ) ) { $this->{$key} = $value; } break; case 'id': $this->{$key} = (int) $value; break; case 'min_charge_amount': $this->min_charge_amount = $value; break; default: if ( is_string( $value ) ) { @json_decode( $value ); if ( json_last_error() !== JSON_ERROR_NONE ) { $this->{$key} = json_decode( $value ); } } $this->{$key} = $value; break; } } /** * Some object vars need to be setup manually as the values need to be * pulled in from the `edd_adjustmentmeta` table. */ $this->excluded_products = (array) edd_get_adjustment_meta( $this->id, 'excluded_product', false ); $this->product_reqs = (array) edd_get_adjustment_meta( $this->id, 'product_requirement', false ); $this->product_condition = (string) edd_get_adjustment_meta( $this->id, 'product_condition', true ); /** * Fires after the instance of the EDD_Discount object is set up. Allows extensions to add items to this object via hook. * * @since 2.7 * * @param object EDD_Discount EDD_Discount instance of the discount object. * @param object WP_Post $discount WP_Post instance of the discount object. */ do_action( 'edd_setup_discount', $this, $discount ); if ( ! empty( $this->id ) ) { return true; } return false; } /** * Helper method to retrieve meta data associated with the discount. * * @since 2.7 * * @param string $key Meta key. * @param bool $single Return single item or array. * * @return mixed */ public function get_meta( $key = '', $single = true ) { return edd_get_adjustment_meta( $this->id, $key, $single ); } /** * Helper method to update meta data associated with the discount. * * @since 2.7 * * @param string $key Meta key to update. * @param string $value New meta value to set. * @param string $prev_value Optional. Previous meta value. * * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure. */ public function update_meta( $key, $value = '', $prev_value = '' ) { $filter_key = '_edd_discount_' . $key; /** * Filters the meta value being updated. * The key is prefixed with `_edd_discount_` for 2.9 backwards compatibility. * * @param mixed $value Value being set. * @param int $id Discount ID. */ $value = apply_filters( 'edd_update_discount_meta_' . $filter_key, $value, $this->id ); return edd_update_adjustment_meta( $this->id, $key, $value, $prev_value ); } /** * Retrieve the code used to apply the discount. * * @since 2.7 * * @return string Discount code. */ public function get_code() { /** * Filters the discount code. * * @since 2.7 * * @param string $code Discount code. * @param int $ID Discount ID. */ return apply_filters( 'edd_get_discount_code', $this->code, $this->id ); } /** * Retrieve the status of the discount * * @since 2.7 * * @return string Discount code status (active/inactive). */ public function get_status() { /** * Filters the discount status. * * @since 2.7 * * @param string $code Discount status (active or inactive). * @param int $ID Discount ID. */ return apply_filters( 'edd_get_discount_status', $this->status, $this->id ); } /** * Retrieves the status label of the discount. * * This method exists as a helper, until legitimate Status classes can be * registered that will contain an array of status-specific labels. * * @since 2.9 * * @return string Status label for the current discount. */ public function get_status_label() { // Default label $label = ucwords( $this->status ); // Specific labels switch ( $this->status ) { case '': $label = __( 'None', 'easy-digital-downloads' ); break; case 'draft': $label = __( 'Draft', 'easy-digital-downloads' ); break; case 'expired': $label = __( 'Expired', 'easy-digital-downloads' ); break; case 'inactive': $label = __( 'Inactive', 'easy-digital-downloads' ); break; case 'active': $label = __( 'Active', 'easy-digital-downloads' ); break; case 'inherit': if ( ! empty( $this->parent ) ) { $parent = edd_get_discount( $this->parent ); $label = $parent->get_status_label(); break; } } /** * Filters the discount status. * * @since 2.9 * * @param string $label Discount status label. * @param string $status Discount status (active or inactive). * @param int $id Discount ID. */ return apply_filters( 'edd_get_discount_status_label', $label, $this->status, $this->id ); } /** * Retrieve the type of discount. * * @since 2.7 * * @return string Discount type (percent or flat amount). */ public function get_type() { /** * Filters the discount type. * * @since 2.7 * * @param string $code Discount type (percent or flat amount). * @param int $ID Discount ID. */ return apply_filters( 'edd_get_discount_type', $this->amount_type, $this->id ); } /** * Retrieve the discount amount. * * @since 2.7 * * @return mixed float Discount amount. */ public function get_amount() { /** * Filters the discount amount. * * @since 2.7 * * @param float $amount Discount amount. * @param int $ID Discount ID. */ return (float) apply_filters( 'edd_get_discount_amount', $this->amount, $this->id ); } /** * Retrieve the discount requirements for the discount to be satisfied. * * @since 2.7 * * @return array IDs of required downloads. */ public function get_product_reqs() { /** * Filters the download requirements. * * @since 2.7 * * @param array $product_reqs IDs of required products. * @param int $ID Discount ID. */ return (array) apply_filters( 'edd_get_discount_product_reqs', $this->product_reqs, $this->id ); } /** * Retrieve the discount scope. * * This used to be called "is_not_global". That filter is still here for backwards compatibility. * * @since 3.0 * * @return string The scope, i.e. "global". */ public function get_scope() { $legacy_value = apply_filters( 'edd_discount_is_not_global', null, $this->id ); if ( ! is_null( $legacy_value ) ) { $this->scope = $legacy_value ? 'global' : 'not_global'; } return apply_filters( 'edd_get_discount_scope', $this->scope, $this->id ); } /** * Retrieve the product condition. * * @since 2.7 * * @return string Product condition */ public function get_product_condition() { /** * Filters the product condition. * * @since 2.7 * * @param string $product_condition Product condition. * @param int $ID Discount ID. */ return apply_filters( 'edd_discount_product_condition', $this->product_condition, $this->id ); } /** * Retrieve the downloads that are excluded from having this discount code applied. * * @since 2.7 * * @return array IDs of excluded downloads. */ public function get_excluded_products() { /** * Filters the excluded downloads. * * @since 2.7 * * @param array $excluded_products IDs of excluded products. * @param int $ID Discount ID. */ return (array) apply_filters( 'edd_get_discount_excluded_products', $this->excluded_products, $this->id ); } /** * Retrieve the start date. * * @since 2.7 * * @return string Start date. */ public function get_start_date() { /** * Filters the start date. * * @since 2.7 * * @param string $start Discount start date. * @param int $ID Discount ID. */ return apply_filters( 'edd_get_discount_start', $this->start_date, $this->id ); } /** * Retrieve the end date. * * @since 2.7 * * @return string End date. */ public function get_expiration() { /** * Filters the end date. * * @since 2.7 * * @param string $expiration Discount expiration date. * @param int $ID Discount ID. */ return apply_filters( 'edd_get_discount_expiration', $this->end_date, $this->id ); } /** * Retrieve the uses for the discount code. * * @since 2.7 * * @return int Uses. */ public function get_uses() { /** * Filters the maximum uses. * * @since 2.7 * * @param int $max_uses Maximum uses. * @param int $ID Discount ID. */ return (int) apply_filters( 'edd_get_discount_uses', $this->use_count, $this->id ); } /** * Retrieve the maximum uses for the discount code. * * @since 2.7 * * @return int Maximum uses. */ public function get_max_uses() { /** * Filters the maximum uses. * * @since 2.7 * * @param int $max_uses Maximum uses. * @param int $ID Discount ID. */ return (int) apply_filters( 'edd_get_discount_max_uses', $this->max_uses, $this->id ); } /** * Retrieve the minimum spend required for the discount to be satisfied. * * @since 2.7 * * @return mixed float Minimum spend. */ public function get_min_price() { /** * Filters the minimum price. * * @since 2.7 * * @param float $min_price Minimum price. * @param int $ID Discount ID. */ return (float) apply_filters( 'edd_get_discount_min_price', $this->min_charge_amount, $this->id ); } /** * Retrieve the usage limit per limit (if the discount can only be used once per customer). * * @since 2.7 * * @return bool Once use per customer? */ public function get_is_single_use() { return $this->get_once_per_customer(); } /** * Retrieve the usage limit per limit (if the discount can only be used once per customer). * * @since 3.0 * * @return bool Once use per customer? */ public function get_once_per_customer() { /** * Filters the single use meta value. * * @since 2.7 * * @param bool $is_single_use Is the discount only allowed to be used once per customer. * @param int $ID Discount ID. */ return (bool) apply_filters( 'edd_is_discount_single_use', $this->once_per_customer, $this->id ); } /** * Check if a discount exists. * * @since 2.7 * * @return bool Discount exists. */ public function exists() { if ( ! $this->id > 0 ) { return false; } return true; } /** * Once object variables has been set, an update is needed to persist them to the database. * * This is now simply a wrapper to the add() method which handles creating new discounts and updating existing ones. * * @since 2.7 * * @return bool True if the save was successful, false if it failed or wasn't needed. */ public function save() { $args = get_object_vars( $this ); $saved = $this->add( $args ); return $saved; } /** * Create a new discount. If the discount already exists in the database, update it. * * @since 2.7 * * @param array $args Discount details. * @return mixed bool|int false if data isn't passed and class not instantiated for creation, or post ID for the new discount. */ public function add( $args = array() ) { // If no code is provided, return early with false if ( empty( $args['code'] ) ) { return false; } if ( ! empty( $this->id ) && $this->exists() ) { return $this->update( $args ); } else { $args = self::convert_legacy_args( $args ); if ( ! empty( $args['start_date'] ) ) { $args['start_date'] = date( 'Y-m-d H:i:s', strtotime( $args['start_date'], current_time( 'timestamp' ) ) ); } if ( ! empty( $args['end_date'] ) ) { $args['end_date'] = date( 'Y-m-d H:i:s', strtotime( $args['end_date'], current_time( 'timestamp' ) ) ); if ( strtotime( $args['end_date'], current_time( 'timestamp' ) ) < current_time( 'timestamp' ) ) { $args['status'] = 'expired'; } } if ( ! empty( $args['start_date'] ) && ! empty( $args['end_date'] ) ) { $start_timestamp = strtotime( $args['start_date'], current_time( 'timestamp' ) ); $end_timestamp = strtotime( $args['end_date'], current_time( 'timestamp' ) ); if ( $start_timestamp > $end_timestamp ) { // Set the expiration date to the start date if start is later than expiration $args['end_date'] = $args['start_date']; } } // Assume discount status is "active" if it has not been set if ( ! isset( $args['status'] ) ) { $args['status'] = 'active'; } /** * Add a new discount to the database. */ /** * Filters the args before being inserted into the database. * * @since 2.7 * * @param array $args Discount args. */ $args = apply_filters( 'edd_insert_discount', $args ); /** * Filters the args before being inserted into the database (kept for backwards compatibility purposes) * * @since 2.7 * @since 3.0 Updated parameters to pass $args twice for backwards compatibility. * * @param array $args Discount args. */ $args = apply_filters( 'edd_insert_discount_args', $args, $args ); $args = $this->sanitize_columns( $args ); /** * Fires before the discount has been added to the database. * * @since 2.7 * * @param array $args Discount args. */ do_action( 'edd_pre_insert_discount', $args ); foreach ( $args as $key => $value ) { $this->{$key} = $value; } // We have to ensure an ID is not passed to edd_add_discount() unset( $args['id'] ); $id = edd_add_discount( $args ); // The DB class 'add' implies an update if the discount being asked to be created already exists if ( ! empty( $id ) ) { // We need to update the ID of the instance of the object in order to add meta $this->id = $id; if ( isset( $args['excluded_products'] ) ) { if ( is_array( $args['excluded_products'] ) ) { foreach ( $args['excluded_products'] as $product ) { edd_add_adjustment_meta( $this->id, 'excluded_product', absint( $product ) ); } } } if ( isset( $args['product_reqs'] ) ) { if ( is_array( $args['product_reqs'] ) ) { foreach ( $args['product_reqs'] as $product ) { edd_add_adjustment_meta( $this->id, 'product_requirement', absint( $product ) ); } } } } /** * Fires after the discount code is inserted. * * @since 2.7 * * @param array $meta { * The discount details. * * @type string $code The discount code. * @type string $name The name of the discount. * @type string $status The discount status. Defaults to active. * @type int $uses The current number of uses. * @type int $max_uses The max number of uses. * @type string $start The start date. * @type int $min_price The minimum price required to use the discount code. * @type array $product_reqs The product IDs required to use the discount code. * @type string $product_condition The conditions in which a product(s) must meet to use the discount code. * @type array $excluded_products Product IDs excluded from this discount code. * @type bool $is_not_global If the discount code is not globally applied to all products. Defaults to false. * @type bool $is_single_use If the code cannot be used more than once per customer. Defaults to false. * } * @param int $ID The ID of the discount that was inserted. */ do_action( 'edd_post_insert_discount', $args, $this->id ); // Discount code created return $id; } } /** * Update an existing discount in the database. * * @since 2.7 * * @param array $args Discount details. * @return bool True if update is successful, false otherwise. */ public function update( $args = array() ) { $args = self::convert_legacy_args( $args ); $ret = false; /** * Filter the data being updated * * @since 2.7 * * @param array $args Discount args. * @param int $ID Discount ID. */ $args = apply_filters( 'edd_update_discount', $args, $this->id ); $args = $this->sanitize_columns( $args ); // Get current time once to avoid inconsistencies $current_time = current_time( 'timestamp' ); if ( ! empty( $args['start_date'] ) && ! empty( $args['end_date'] ) ) { $start_timestamp = strtotime( $args['start_date'], $current_time ); $end_timestamp = strtotime( $args['end_date'], $current_time ); // Set the expiration date to the start date if start is later than expiration if ( $start_timestamp > $end_timestamp ) { $args['end_date'] = $args['start_date']; } } // Start date if ( ! empty( $args['start_date'] ) ) { $args['start_date'] = date( 'Y-m-d H:i:s', strtotime( $args['start_date'], $current_time ) ); } // End date if ( ! empty( $args['end_date'] ) ) { $args['end_date'] = date( 'Y-m-d H:i:s', strtotime( $args['end_date'], $current_time ) ); } if ( isset( $args['excluded_products'] ) ) { // Reset meta edd_delete_adjustment_meta( $this->id, 'excluded_product' ); if ( is_array( $args['excluded_products'] ) ) { // Now add each newly excluded product foreach ( $args['excluded_products'] as $product ) { edd_add_adjustment_meta( $this->id, 'excluded_product', absint( $product ) ); } } } if ( isset( $args['product_reqs'] ) ) { // Reset meta edd_delete_adjustment_meta( $this->id, 'product_requirement' ); if ( is_array( $args['product_reqs'] ) ) { // Now add each newly required product foreach ( $args['product_reqs'] as $product ) { edd_add_adjustment_meta( $this->id, 'product_requirement', absint( $product ) ); } } } // Switch `type` to `amount_type` if ( ! isset( $args['amount_type'] ) && ! empty( $args['type'] ) && 'discount' !== $args['type'] ) { $args['amount_type'] = $args['type']; } // Force `type` to `discount` $args['type'] = 'discount'; /** * Fires before the discount has been updated in the database. * * @since 2.7 * * @param array $args Discount args. * @param int $ID Discount ID. */ do_action( 'edd_pre_update_discount', $args, $this->id ); // If we are using the discounts DB if ( edd_update_discount( $this->id, $args ) ) { $discount = edd_get_discount( $this->id ); $this->setup_discount( $discount ); $ret = true; } /** * Fires after the discount has been updated in the database. * * @since 2.7 * * @param array $args Discount args. * @param int $ID Discount ID. */ do_action( 'edd_post_update_discount', $args, $this->id ); return $ret; } /** * Update the status of the discount. * * @since 2.7 * * @param string $new_status New status (default: active) * @return bool If the status been updated or not. */ public function update_status( $new_status = 'active' ) { /** * Fires before the status of the discount is updated. * * @since 2.7 * * @param int $ID Discount ID. * @param string $new_status New status. * @param string $post_status Post status. */ do_action( 'edd_pre_update_discount_status', $this->id, $new_status, $this->status ); $ret = $this->update( array( 'status' => $new_status ) ); /** * Fires after the status of the discount is updated. * * @since 2.7 * * @param int $ID Discount ID. * @param string $new_status New status. * @param string $status Post status. */ do_action( 'edd_post_update_discount_status', $this->id, $new_status, $this->status ); return (bool) $ret; } /** * Check if the discount has started. * * @since 2.7 * * @param bool $set_error Whether an error message be set in session. * @return bool Is discount started? */ public function is_started( $set_error = true ) { $return = false; if ( $this->start_date ) { $start_date = strtotime( $this->start_date ); if ( $start_date < time() ) { // Discount has pased the start date $return = true; } elseif ( $set_error ) { edd_set_error( 'edd-discount-error', _x( 'This discount is invalid.', 'error shown when attempting to use a discount before its start date', 'easy-digital-downloads' ) ); } } else { // No start date for this discount, so has to be true $return = true; } /** * Filters if the discount has started or not. * * @since 2.7 * * @param bool $return Has the discount started or not. * @param int $ID Discount ID. * @param bool $set_error Whether an error message be set in session. */ return apply_filters( 'edd_is_discount_started', $return, $this->id, $set_error ); } /** * Check if the discount has expired. * * @since 2.7 * * @param bool $update Update the discount to expired if an one is found but has an active status * @return bool Has the discount expired? */ public function is_expired( $update = true ) { $return = false; if ( empty( $this->end_date ) || '0000-00-00 00:00:00' === $this->end_date ) { return $return; } $end_date = strtotime( $this->end_date ); if ( $end_date < time() ) { if ( $update ) { $this->update_status( 'expired' ); } $return = true; } /** * Filters if the discount has expired or not. * * @since 2.7 * * @param bool $return Has the discount expired or not. * @param int $ID Discount ID. */ return apply_filters( 'edd_is_discount_expired', $return, $this->id ); } /** * Check if the discount has maxed out. * * @since 2.7 * * @param bool $set_error Whether an error message be set in session. * @return bool Is discount maxed out? */ public function is_maxed_out( $set_error = true ) { $return = false; if ( $this->uses >= $this->max_uses && ! empty( $this->max_uses ) ) { if ( $set_error ) { edd_set_error( 'edd-discount-error', __( 'This discount has reached its maximum usage.', 'easy-digital-downloads' ) ); } $return = true; } /** * Filters if the discount is maxed out or not. * * @since 2.7 * * @param bool $return Is the discount maxed out or not. * @param int $ID Discount ID. * @param bool $set_error Whether an error message be set in session. */ return apply_filters( 'edd_is_discount_maxed_out', $return, $this->id, $set_error ); } /** * Check if the minimum cart amount is satisfied for the discount to hold. * * @since 2.7 * * @param bool $set_error Whether an error message be set in session. * @return bool Is the minimum cart amount met? */ public function is_min_price_met( $set_error = true ) { $return = false; $cart_amount = edd_get_cart_discountable_subtotal( $this->id ); if ( (float) $cart_amount >= (float) $this->min_charge_amount ) { $return = true; } elseif ( $set_error ) { edd_set_error( 'edd-discount-error', sprintf( __( 'Minimum order of %s not met.', 'easy-digital-downloads' ), edd_currency_filter( edd_format_amount( $this->min_charge_amount ) ) ) ); } /** * Filters if the minimum cart amount has been met to satisfy the discount. * * @since 2.7 * * @param bool $return Is the minimum cart amount met or not. * @param int $ID Discount ID. * @param bool $set_error Whether an error message be set in session. */ return apply_filters( 'edd_is_discount_min_met', $return, $this->id, $set_error ); } /** * Is the discount single use or not? * * @since 2.7 * * @return bool Is the discount single use or not? */ public function is_single_use() { /** * Filters if the discount is single use or not. * * @since 2.7 * * @param bool $single_use Is the discount is single use or not. * @param int $ID Discount ID. */ return (bool) apply_filters( 'edd_is_discount_single_use', $this->once_per_customer, $this->id ); } /** * Are the product requirements met for the discount to hold. * * @since 2.7 * * @param bool $set_error Whether an error message be set in session. * @return bool Are required products in the cart? */ public function is_product_requirements_met( $set_error = true ) { $product_reqs = $this->get_product_reqs(); $excluded_ps = $this->get_excluded_products(); $cart_items = edd_get_cart_contents(); $cart_ids = $cart_items ? wp_list_pluck( $cart_items, 'id' ) : null; $is_met = true; /** * Normalize our data for product requirements, exclusions and cart data. */ // First absint the items, then sort, and reset the array keys $product_reqs = array_map( 'absint', $product_reqs ); asort( $product_reqs ); $product_reqs = array_filter( array_values( $product_reqs ) ); $cart_ids = array_map( 'absint', $cart_ids ); asort( $cart_ids ); $cart_ids = array_values( $cart_ids ); // Ensure we have requirements before proceeding if ( ! empty( $product_reqs ) ) { $matches = array_intersect( $product_reqs, $cart_ids ); switch ( $this->get_product_condition() ) { case 'all': $is_met = count( $matches ) === count( $product_reqs ); break; default: $is_met = 0 < count( $matches ); } if ( ! $is_met && $set_error ) { edd_set_error( 'edd-discount-error', __( 'The product requirements for this discount are not met.', 'easy-digital-downloads' ) ); } } $excluded_ps = array_map( 'absint', $excluded_ps ); asort( $excluded_ps ); $excluded_ps = array_filter( array_values( $excluded_ps ) ); if ( ! empty( $excluded_ps ) ) { if ( count( array_intersect( $cart_ids, $excluded_ps ) ) === count( $cart_ids ) ) { $is_met = false; if ( $set_error ) { edd_set_error( 'edd-discount-error', __( 'This discount is not valid for the cart contents.', 'easy-digital-downloads' ) ); } } } /** * Filters whether the product requirements are met for the discount to hold. * * @since 2.7 * * @param bool $is_met Are the product requirements met or not. * @param int $ID Discount ID. * @param string $product_condition Product condition. * @param bool $set_error Whether an error message be set in session. */ return (bool) apply_filters( 'edd_is_discount_products_req_met', $is_met, $this->id, $this->product_condition, $set_error ); } /** * Has the discount code been used. * * @since 2.7 * @since 3.0 Refactored to use new query methods. * * @param string $user User info. * @param bool $set_error Whether an error message be set in session. * * @return bool Whether the discount has been used or not. */ public function is_used( $user = '', $set_error = true ) { $return = false; if ( $this->is_single_use ) { $payments = array(); if ( edd_get_component_interface( 'customer', 'table' )->exists() ) { $by_user_id = ! is_email( $user ); $customer = new EDD_Customer( $user, $by_user_id ); $payments = explode( ',', $customer->payment_ids ); } else { $user_found = false; if ( is_email( $user ) ) { $user_found = true; // All we need is the email $key = '_edd_payment_user_email'; $value = $user; } else { $user_data = get_user_by( 'login', $user ); if ( $user_data ) { $user_found = true; $key = '_edd_payment_user_id'; $value = $user_data->ID; } } if ( $user_found ) { $query_args = array( 'post_type' => 'edd_payment', 'meta_query' => array( array( 'key' => $key, 'value' => $value, 'compare' => '=', ), ), 'fields' => 'ids', ); $payments = get_posts( $query_args ); // Get all payments with matching email } } if ( $payments ) { foreach ( $payments as $payment ) { $payment = new EDD_Payment( $payment ); if ( empty( $payment->discounts ) ) { continue; } if ( in_array( $payment->status, edd_get_incomplete_order_statuses(), true ) ) { continue; } $discounts = explode( ',', $payment->discounts ); if ( is_array( $discounts ) ) { $discounts = array_map( 'strtoupper', $discounts ); $key = array_search( strtoupper( $this->code ), $discounts, true ); if ( false !== $key ) { if ( $set_error ) { edd_set_error( 'edd-discount-error', __( 'This discount has already been redeemed.', 'easy-digital-downloads' ) ); } $return = true; break; } } } } } /** * Filters if the discount is used or not. * * @since 2.7 * * @param bool $return If the discount is used or not. * @param int $ID Discount ID. * @param string $user User info. * @param bool $set_error Whether an error message be set in session. */ return apply_filters( 'edd_is_discount_used', $return, $this->id, $user, $set_error ); } /** * Checks whether a discount holds at the time of purchase. * * @since 2.7 * * @param string $user User info. * @param bool $set_error Whether an error message be set in session. * @return bool Is the discount valid or not? */ public function is_valid( $user = '', $set_error = true ) { $return = false; $user = trim( $user ); if ( edd_get_cart_contents() && $this->id ) { if ( $this->is_active( true, $set_error ) && $this->is_started( $set_error ) && ! $this->is_maxed_out( $set_error ) && ! $this->is_used( $user, $set_error ) && $this->is_product_requirements_met( $set_error ) && $this->is_min_price_met( $set_error ) ) { $return = true; } } elseif ( $set_error ) { edd_set_error( 'edd-discount-error', _x( 'This discount is invalid.', 'error for when a discount is invalid based on its configuration', 'easy-digital-downloads' ) ); } /** * Filters whether the discount is valid or not. * * @since 2.7 * * @param bool $return If the discount is used or not. * @param int $ID Discount ID. * @param string $code Discount code. * @param string $user User info. * @param bool $set_error Whether an error message be set in session. */ return apply_filters( 'edd_is_discount_valid', $return, $this->id, $this->code, $user, $set_error ); } /** * Checks if a discount code is active. * * @since 2.7 * * @param bool $update Update the discount to expired if an one is found but has an active status. * @param bool $set_error Whether an error message be set in session. * @return bool If the discount is active or not. */ public function is_active( $update = true, $set_error = true ) { $return = false; if ( $this->exists() ) { if ( $this->is_expired( $update ) ) { if ( edd_doing_ajax() && $set_error ) { edd_set_error( 'edd-discount-error', __( 'This discount is expired.', 'easy-digital-downloads' ) ); } } elseif ( 'active' === $this->status ) { $return = true; } elseif ( edd_doing_ajax() && $set_error ) { edd_set_error( 'edd-discount-error', __( 'This discount is not active.', 'easy-digital-downloads' ) ); } } /** * Filters if the discount is active or not. * * @since 2.7 * * @param bool $return Is the discount active or not. * @param int $ID Discount ID. * @param bool $set_error Whether an error message be set in session. */ return apply_filters( 'edd_is_discount_active', $return, $this->id, $set_error ); } /** * Get Discounted Amount. * * @since 2.7 * * @param string|int $base_price Price before discount. * @return float $discounted_price Amount after discount. */ public function get_discounted_amount( $base_price ) { $base_price = floatval( $base_price ); if ( 'flat' === $this->amount_type ) { $amount = $base_price - floatval( $this->amount ); if ( $amount < 0 ) { $amount = 0; } } else { // Percentage discount $amount = $base_price - ( $base_price * ( floatval( $this->amount ) / 100 ) ); } /** * Filter the discounted amount calculated. * * @since 2.7 * @access public * * @param float $amount Calculated discounted amount. * @param EDD_Discount $this Discount object. */ return apply_filters( 'edd_discounted_amount', $amount, $this ); } /** * Increment the usage of the discount. * * @since 2.7 * * @return int New discount usage. */ public function increase_usage() { if ( $this->get_uses() ) { $this->use_count++; } else { $this->use_count = 1; } $args = array( 'use_count' => $this->use_count ); $this->max_uses = absint( $this->max_uses ); if ( 0 !== $this->max_uses && $this->max_uses <= $this->use_count ) { $args['status'] = 'inactive'; } $this->update( $args ); /** * Fires after the usage count has been increased. * * @since 2.7 * * @param int $use_count Discount usage. * @param int $ID Discount ID. * @param string $code Discount code. */ do_action( 'edd_discount_increase_use_count', $this->use_count, $this->id, $this->code ); return (int) $this->use_count; } /** * Decrement the usage of the discount. * * @since 2.7 * * @return int New discount usage. */ public function decrease_usage() { if ( $this->get_uses() ) { $this->use_count--; } if ( $this->use_count < 0 ) { $this->use_count = 0; } $args = array( 'use_count' => $this->use_count ); if ( 0 !== $this->max_uses && $this->max_uses > $this->use_count ) { $args['status'] = 'active'; } $this->update( $args ); /** * Fires after the usage count has been decreased. * * @since 2.7 * * @param int $use_count Discount usage. * @param int $ID Discount ID. * @param string $code Discount code. */ do_action( 'edd_discount_decrease_use_count', $this->use_count, $this->id, $this->code ); return (int) $this->use_count; } /** * Edit Discount Link. * * @since 2.7 * * @return string Link to the `Edit Discount` page. */ public function edit_url() { return esc_url( edd_get_admin_url( array( 'page' => 'edd-discounts', 'edd-action' => 'edit_discount', 'discount' => absint( $this->id ), ) ) ); } /** * Sanitize the data for update/create * * @since 3.0 * @param array $data The data to sanitize * @return array The sanitized data, based off column defaults */ private function sanitize_columns( $data ) { $default_values = array(); foreach ( $data as $key => $type ) { // Only sanitize data that we were provided if ( ! array_key_exists( $key, $data ) ) { continue; } switch ( $type ) { case '%s': if ( 'email' === $key ) { $data[ $key ] = sanitize_email( $data[ $key ] ); } elseif ( 'notes' === $key ) { $data[ $key ] = strip_tags( $data[ $key ] ); } else { if ( is_array( $data[ $key ] ) ) { $data[ $key ] = json_encode( $data[ $key ] ); } else { $data[ $key ] = sanitize_text_field( $data[ $key ] ); } } break; case '%d': if ( ! is_numeric( $data[ $key ] ) || absint( $data[ $key ] ) !== (int) $data[ $key ] ) { $data[ $key ] = $default_values[ $key ]; } else { $data[ $key ] = absint( $data[ $key ] ); } break; case '%f': // Convert what was given to a float $value = floatval( $data[ $key ] ); if ( ! is_float( $value ) ) { $data[ $key ] = $default_values[ $key ]; } else { $data[ $key ] = $value; } break; default: $data[ $key ] = ! is_array( $data[ $key ] ) ? sanitize_text_field( $data[ $key ] ) : maybe_serialize( array_map( 'sanitize_text_field', $data[ $key ] ) ); break; } } return $data; } /** * Converts pre-3.0 arguments to the 3.0+ version. * * @since 3.0 * @static * * @param $args array Arguments to be converted. * @return array The converted arguments. */ public static function convert_legacy_args( $args = array() ) { // Loop through arguments provided and adjust old key names for the new schema introduced in 3.0 $old = array( 'uses' => 'use_count', 'max' => 'max_uses', 'start' => 'start_date', 'expiration' => 'end_date', 'min_price' => 'min_charge_amount', 'products' => 'product_reqs', 'excluded-products' => 'excluded_products', 'not_global' => 'scope', 'is_not_global' => 'scope', 'use_once' => 'once_per_customer', 'is_single_use' => 'once_per_customer', ); foreach ( $old as $old_key => $new_key ) { if ( isset( $args[ $old_key ] ) ) { if ( in_array( $old_key, array( 'not_global', 'is_not_global' ), true ) && ! array_key_exists( 'scope', $args ) ) { $args[ $new_key ] = ! empty( $args[ $old_key ] ) ? 'not_global' : 'global'; } else { $args[ $new_key ] = $args[ $old_key ]; } } unset( $args[ $old_key ] ); } // Default status needs to be active for regression purposes. // See https://github.com/easydigitaldownloads/easy-digital-downloads/issues/6806 if ( ! isset( $args['status'] ) ) { $args['status'] = 'active'; } return $args; } }