laipower/wp-content/plugins/easy-digital-downloads/includes/class-edd-discount.php

1911 lines
47 KiB
PHP

<?php
/**
* Discount Object
*
* @package EDD
* @subpackage Discounts
* @copyright Copyright (c) 2018, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 2.7
*/
use EDD\Database\Rows\Adjustment;
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
/**
* EDD_Discount Class
*
* @since 2.7
* @since 3.0 Extends EDD\Database\Rows\Adjustment instead of EDD_DB_Discount
*
* @property int $id
* @property string $name
* @property string $code
* @property string $status
* @property string $amount_type
* @property float $amount
* @property array $product_reqs
* @property string $scope
* @property array $excluded_products
* @property string $product_condition
* @property string $date_created
* @property string $date_modified
* @property string $start_date
* @property string $end_date
* @property int $use_count
* @property int $max_uses
* @property float $min_charge_amount
* @property bool $once_per_customer
*/
class EDD_Discount extends Adjustment {
/**
* Flat discount.
*
* @since 3.0
* @var string
*/
const FLAT = 'flat';
/**
* Percent discount.
*
* @since 3.0
* @var string
*/
const PERCENT = 'percent';
/**
* Discount ID.
*
* @since 2.7
* @access protected
* @var int
*/
protected $id = 0;
/**
* Discount Name.
*
* @since 2.7
* @access protected
* @var string
*/
protected $name = null;
/**
* Discount Code.
*
* @since 2.7
* @access protected
* @var string
*/
protected $code = null;
/**
* Discount Status (Active or Inactive).
*
* @since 2.7
* @access protected
* @var string
*/
protected $status = null;
/**
* Discount Type (Percentage or Flat Amount).
*
* @since 3.0
* @access protected
* @var string
*/
protected $amount_type = null;
/**
* Discount Amount.
*
* @since 2.7
* @access protected
* @var mixed float|int
*/
protected $amount = null;
/**
* Download Requirements.
*
* @since 2.7
* @access protected
* @var array
*/
protected $product_reqs = array();
/**
* Scope of the discount.
*
* global - Applies to all products in the cart, save for those explicitly excluded through excluded_products
* not_global - Applies only to the products set in product_reqs
*
* This used to be called "is_not_global" but was changed to "scope" in 3.0.
*
* @since 3.0
* @access protected
* @var string
*/
protected $scope = null;
/**
* Excluded Downloads.
*
* @since 2.7
* @access protected
* @var array
*/
protected $excluded_products = array();
/**
* Product Condition
*
* @since 2.7
* @access protected
* @var string
*/
protected $product_condition = null;
/**
* Created Date.
*
* @since 3.0
* @access protected
* @var string
*/
protected $date_created = null;
/**
* Modified Date.
*
* @since 3.0
* @access protected
* @var string
*/
protected $date_modified = null;
/**
* Start Date.
*
* @since 2.7
* @access protected
* @var string
*/
protected $start_date = null;
/**
* End Date.
*
* @since 2.7
* @access protected
* @var string
*/
protected $end_date = null;
/**
* Uses.
*
* @since 2.7
* @access protected
* @var int
*/
protected $use_count = null;
/**
* Maximum Uses.
*
* @since 2.7
* @access protected
* @var int
*/
protected $max_uses = null;
/**
* Minimum Amount.
*
* @since 2.7
* @access protected
* @var mixed int|float
*/
protected $min_charge_amount = null;
/**
* Is Single Use per customer?
*
* @since 2.7
* @access protected
* @var bool
*/
protected $once_per_customer = null;
/**
* Constructor.
*
* @since 2.7
* @access protected
*
* @param mixed int|string $_id_or_code_or_name Discount id/code/name.
* @param bool $by_code Whether identifier passed was a discount code.
* @param bool $by_name Whether identifier passed was a discount name.
*/
public function __construct( $_id_or_code_or_name = false, $by_code = false, $by_name = false ) {
// Bail if no id or code
if ( empty( $_id_or_code_or_name ) ) {
return false;
}
// Already an object
if ( is_object( $_id_or_code_or_name ) ) {
$discount = $_id_or_code_or_name;
// Code
} elseif ( $by_code ) {
$discount = $this->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;
}
}