File: /home/ukqcurpj/www/wp-content/plugins/paid-memberships-pro/classes/class-pmpro-subscription.php
<?php
/**
* The PMPro Subscription object.
*
* @method int get_id Get the ID of the subscription.
* @method int get_user_id Get the ID of the user the subscription belongs to.
* @method int get_membership_level_id Get the ID of the membership level that this subscription is for.
* @method string get_gateway Get the gateway used to create the subscription.
* @method string get_gateway_environment Get the gateway environment used to create the subscription.
* @method string get_subscription_transaction_id Get the ID of the subscription in the gateway.
* @method string get_status Get the status of the subscription.
* @method float get_billing_amount Get the billing amount.
* @method int get_cycle_number Get the number of cycles.
* @method string get_cycle_period Get the cycle period.
* @method int get_billing_limit Get the billing limit.
* @method float get_trial_amount Get the trial amount.
* @method int get_trial_limit Get the trial limit.
*
* @since 3.0
*/
class PMPro_Subscription {
/**
* The subscription ID.
*
* @since 3.0
*
* @var int
*/
protected $id = 0;
/**
* The subscription user ID.
*
* @since 3.0
*
* @var int
*/
protected $user_id = 0;
/**
* The subscription membership level ID.
*
* @since 3.0
*
* @var int
*/
protected $membership_level_id = 0;
/**
* The subscription gateway.
*
* @since 3.0
*
* @var string
*/
protected $gateway = '';
/**
* The subscription gateway environment.
*
* @since 3.0
*
* @var string
*/
protected $gateway_environment = '';
/**
* The subscription transaction id.
*
* @since 3.0
*
* @var string
*/
protected $subscription_transaction_id = '';
/**
* The subscription status.
*
* @since 3.0
*
* @var string
*/
protected $status = '';
/**
* The subscription start date (UTC YYYY-MM-DD HH:MM:SS).
*
* @since 3.0
*
* @var string
*/
protected $startdate = '';
/**
* The subscription end date (UTC YYYY-MM-DD HH:MM:SS).
*
* @since 3.0
*
* @var string
*/
protected $enddate = '';
/**
* The subscription next payment date (UTC YYYY-MM-DD HH:MM:SS).
*
* @since 3.0
*
* @var string
*/
protected $next_payment_date = '';
/**
* The subscription billing amount.
*
* @since 3.0
*
* @var float
*/
protected $billing_amount = 0.00;
/**
* The subscription billing cycle number.
*
* @since 3.0
*
* @var int
*/
protected $cycle_number = 0;
/**
* The subscription billing cycle period.
*
* @since 3.0
*
* @var string
*/
protected $cycle_period = 'Month';
/**
* The subscription billing limit.
*
* @since 3.0
*
* @var int
*/
protected $billing_limit = 0;
/**
* The subscription trial billing amount.
*
* @since 3.0
*
* @var float
*/
protected $trial_amount = 0.00;
/**
* The subscription trial billing cycle number.
*
* @since 3.0
*
* @var int
*/
protected $trial_limit = 0;
/**
* The initial payment amount for this subscription.
*
* This will be filled in automatically when $sub->get_initial_payment() is called.
*
* @since 3.0
*
* @var null|float|int
* @see get_initial_payment()
*/
protected $initial_payment;
/**
* The date that this subscription was last modified.
*
* @since 3.0
*
* @var string
*/
protected $modified = '';
/**
* Create a new PMPro_Subscription object.
*
* @since 3.0
*
* @param null|int|array|object $subscription The ID of the subscription to set up or the subscription data to load.
* Leave empty for a new subscription.
*/
public function __construct( $subscription = null ) {
global $wpdb;
if ( empty( $subscription ) ) {
return;
}
$subscription_data = [];
if ( is_numeric( $subscription ) ) {
// Get an existing subscription.
$subscription_data = $wpdb->get_row(
$wpdb->prepare(
"
SELECT *
FROM $wpdb->pmpro_subscriptions
WHERE id = %d
",
$subscription
),
ARRAY_A
);
} elseif ( is_array( $subscription ) ) {
$subscription_data = $subscription;
} elseif ( is_object( $subscription ) ) {
$subscription_data = get_object_vars( $subscription );
} else {
// Invalid $subscription so there's nothing we can do.
return;
}
if ( ! empty( $subscription_data ) ) {
$this->set( $subscription_data );
}
// Check if this subscription has default migration data.
$this->maybe_fix_default_migration_data();
}
/**
* Call magic methods.
*
* @since 3.0
*
* @param string $name The method that was called.
* @param array $arguments The arguments passed to the method.
*
* @return mixed|null
*/
public function __call( $name, $arguments ) {
if ( 0 === strpos( $name, 'get_' ) ) {
$property_name_arr = explode( 'get_', $name );
$property_name = $property_name_arr[1];
$supported_properties = [
'id',
'user_id',
'membership_level_id',
'gateway',
'gateway_environment',
'subscription_transaction_id',
'status',
'billing_amount',
'cycle_number',
'cycle_period',
'billing_limit',
'trial_amount',
'trial_limit',
];
if ( in_array( $property_name, $supported_properties, true ) ) {
return $this->{$property_name};
}
}
return null;
}
/**
* Get the subscription object based on query arguments.
*
* @param int|array $args The query arguments to use or the subscription ID.
*
* @return PMPro_Subscription|null The subscription objects or null if not found.
*/
public static function get_subscription( $args = [] ) {
// At least one argument is required.
if ( empty( $args ) ) {
return null;
}
if ( is_numeric( $args ) ) {
$args = [
'id' => $args,
];
}
// Invalid arguments.
if ( ! is_array( $args ) ) {
return null;
}
// Force returning of one subscription.
$args['limit'] = 1;
// Get the subscriptions using query arguments.
$subscriptions = self::get_subscriptions( $args );
// Check if we found any subscriptions.
if ( empty( $subscriptions ) ) {
return null;
}
// Get the first subscription in the array.
return reset( $subscriptions );
}
/**
* Get the list of subscription objects based on query arguments.
*
* Defaults to returning the latest 100 subscriptions.
*
* @param array $args The query arguments to use.
*
* @return PMPro_Subscription[] The list of subscription objects.
*/
public static function get_subscriptions( array $args = [] ) {
global $wpdb;
$sql_query = "SELECT `id` FROM `$wpdb->pmpro_subscriptions`";
$prepared = [];
$where = [];
$orderby = isset( $args['orderby'] ) ? $args['orderby'] : '`startdate` DESC';
$limit = isset( $args['limit'] ) ? (int) $args['limit'] : 100;
// Detect unsupported orderby usage (in the future we may support better syntax).
if ( $orderby !== preg_replace( '/[^a-zA-Z0-9\s,`]/', ' ', $orderby ) ) {
return [];
}
/*
* Now filter the query based on the arguments provided.
*/
// Filter by ID(s).
if ( isset( $args['id'] ) ) {
if ( ! is_array( $args['id'] ) ) {
$where[] = 'id = %d';
$prepared[] = $args['id'];
} else {
$where[] = 'id IN ( ' . implode( ', ', array_fill( 0, count( $args['id'] ), '%d' ) ) . ' )';
$prepared = array_merge( $prepared, $args['id'] );
}
}
// Filter by user ID(s).
if ( isset( $args['user_id'] ) ) {
if ( ! is_array( $args['user_id'] ) ) {
$where[] = 'user_id = %d';
$prepared[] = $args['user_id'];
} else {
$where[] = 'user_id IN ( ' . implode( ', ', array_fill( 0, count( $args['user_id'] ), '%d' ) ) . ' )';
$prepared = array_merge( $prepared, $args['user_id'] );
}
}
// Filter by membership level ID(s).
if ( isset( $args['membership_level_id'] ) ) {
if ( ! is_array( $args['membership_level_id'] ) ) {
$where[] = 'membership_level_id = %d';
$prepared[] = $args['membership_level_id'];
} else {
$where[] = 'membership_level_id IN ( ' . implode( ', ', array_fill( 0, count( $args['membership_level_id'] ), '%d' ) ) . ' )';
$prepared = array_merge( $prepared, $args['membership_level_id'] );
}
}
// Filter by status(es).
if ( isset( $args['status'] ) ) {
if ( ! is_array( $args['status'] ) ) {
$where[] = 'status = %s';
$prepared[] = $args['status'];
} else {
$where[] = 'status IN ( ' . implode( ', ', array_fill( 0, count( $args['status'] ), '%s' ) ) . ' )';
$prepared = array_merge( $prepared, $args['status'] );
}
}
// Filter by subscription transaction ID(s).
if ( isset( $args['subscription_transaction_id'] ) ) {
if ( ! is_array( $args['subscription_transaction_id'] ) ) {
$where[] = 'subscription_transaction_id = %s';
$prepared[] = $args['subscription_transaction_id'];
} else {
$where[] = 'subscription_transaction_id IN ( ' . implode( ', ', array_fill( 0, count( $args['subscription_transaction_id'] ), '%s' ) ) . ' )';
$prepared = array_merge( $prepared, $args['subscription_transaction_id'] );
}
}
// Filter by gateway(s).
if ( isset( $args['gateway'] ) ) {
if ( ! is_array( $args['gateway'] ) ) {
$where[] = 'gateway = %s';
$prepared[] = $args['gateway'];
} else {
$where[] = 'gateway IN ( ' . implode( ', ', array_fill( 0, count( $args['gateway'] ), '%s' ) ) . ' )';
$prepared = array_merge( $prepared, $args['gateway'] );
}
}
// Filter by gateway environment(s).
if ( isset( $args['gateway_environment'] ) ) {
if ( ! is_array( $args['gateway_environment'] ) ) {
$where[] = 'gateway_environment = %s';
$prepared[] = $args['gateway_environment'];
} else {
$where[] = 'gateway_environment IN ( ' . implode( ', ', array_fill( 0, count( $args['gateway_environment'] ), '%s' ) ) . ' )';
$prepared = array_merge( $prepared, $args['gateway_environment'] );
}
}
// Filter by billing amount(s).
if ( isset( $args['billing_amount'] ) ) {
if ( ! is_array( $args['billing_amount'] ) ) {
$where[] = 'billing_amount = %f';
$prepared[] = $args['billing_amount'];
} else {
$where[] = 'billing_amount IN ( ' . implode( ', ', array_fill( 0, count( $args['billing_amount'] ), '%f' ) ) . ' )';
$prepared = array_merge( $prepared, $args['billing_amount'] );
}
}
// Filter by cycle number(s).
if ( isset( $args['cycle_number'] ) ) {
if ( ! is_array( $args['cycle_number'] ) ) {
$where[] = 'cycle_number = %d';
$prepared[] = $args['cycle_number'];
} else {
$where[] = 'cycle_number IN ( ' . implode( ', ', array_fill( 0, count( $args['cycle_number'] ), '%d' ) ) . ' )';
$prepared = array_merge( $prepared, $args['cycle_number'] );
}
}
// Filter by cycle period(s).
if ( isset( $args['cycle_period'] ) ) {
if ( ! is_array( $args['cycle_period'] ) ) {
$where[] = 'cycle_period = %s';
$prepared[] = $args['cycle_period'];
} else {
$where[] = 'cycle_period IN ( ' . implode( ', ', array_fill( 0, count( $args['cycle_period'] ), '%s' ) ) . ' )';
$prepared = array_merge( $prepared, $args['cycle_period'] );
}
}
// Filter by billing limit(s).
if ( isset( $args['billing_limit'] ) ) {
if ( ! is_array( $args['billing_limit'] ) ) {
$where[] = 'billing_limit = %d';
$prepared[] = $args['billing_limit'];
} else {
$where[] = 'billing_limit IN ( ' . implode( ', ', array_fill( 0, count( $args['billing_limit'] ), '%d' ) ) . ' )';
$prepared = array_merge( $prepared, $args['billing_limit'] );
}
}
// Filter by trial amount(s).
if ( isset( $args['trial_amount'] ) ) {
if ( ! is_array( $args['trial_amount'] ) ) {
$where[] = 'trial_amount = %f';
$prepared[] = $args['trial_amount'];
} else {
$where[] = 'trial_amount IN ( ' . implode( ', ', array_fill( 0, count( $args['trial_amount'] ), '%f' ) ) . ' )';
$prepared = array_merge( $prepared, $args['trial_amount'] );
}
}
// Filter by trial limit(s).
if ( isset( $args['trial_limit'] ) ) {
if ( ! is_array( $args['trial_limit'] ) ) {
$where[] = 'trial_limit = %d';
$prepared[] = $args['trial_limit'];
} else {
$where[] = 'trial_limit IN ( ' . implode( ', ', array_fill( 0, count( $args['trial_limit'] ), '%d' ) ) . ' )';
$prepared = array_merge( $prepared, $args['trial_limit'] );
}
}
// Maybe filter the data.
if ( $where ) {
$sql_query .= ' WHERE ' . implode( ' AND ', $where );
}
// Handle the order of data.
$sql_query .= ' ORDER BY ' . $orderby;
// Maybe limit the data.
if ( $limit ) {
$sql_query .= ' LIMIT %d';
$prepared[] = $limit;
}
// Maybe prepare the query.
if ( $prepared ) {
$sql_query = $wpdb->prepare( $sql_query, $prepared );
}
$subscription_ids = $wpdb->get_col( $sql_query );
if ( empty( $subscription_ids ) ) {
return [];
}
$subscriptions = [];
foreach ( $subscription_ids as $subscription_id ) {
$subscription = new PMPro_Subscription( $subscription_id );
// Make sure the subscription object is valid.
if ( ! empty( $subscription->id ) ) {
$subscriptions[] = $subscription;
}
}
return $subscriptions;
}
/**
* Get subscriptions for a user.
*
* @since 3.0
*
* @param int|null $user_id ID of the user to get subscriptions for. Defaults to current user.
* @param int|int[]|null $membership_level_id The membership level ID(s) to get subscriptions for. Defaults to all.
* @param string|string[]|null $status The status(es) of the subscription to get. Defaults to active.
*
* @return PMPro_Subscription[] The list of subscription objects.
*/
public static function get_subscriptions_for_user( $user_id = null, $membership_level_id = null, $status = [ 'active' ] ) {
// Get user_id if none passed.
if ( empty( $user_id ) ) {
$user_id = get_current_user_id();
}
// Check for a valid user.
if ( empty( $user_id ) ) {
return [];
}
// Filter by user ID.
$args = [
'user_id' => $user_id,
];
// Filter by membership level ID(s).
if ( $membership_level_id ) {
$args['membership_level_id'] = $membership_level_id;
}
// Filter by status(es).
if ( $status ) {
$args['status'] = $status;
}
return self::get_subscriptions( $args );
}
/**
* Get the subscription with the given subscription transaction ID.
*
* @since 3.0
*
* @param string $subscription_transaction_id Subscription transaction ID to get.
* @param string $gateway Gateway to get the subscription for.
* @param string $gateway_environment Gateway environment to get the subscription for.
*
* @return PMPro_Subscription|null PMPro_Subscription object if found, null if not found.
*/
public static function get_subscription_from_subscription_transaction_id( $subscription_transaction_id, $gateway, $gateway_environment ) {
// Require subscriptio transaction ID.
if ( empty( $subscription_transaction_id ) ) {
return null;
}
// Filter by args specified.
$args = [
'subscription_transaction_id' => $subscription_transaction_id,
'gateway' => $gateway,
'gateway_environment' => $gateway_environment,
];
return self::get_subscription( $args );
}
/**
* Create a new subscription.
*
* @since 3.0
*
* @param array $args {
* Arguments to create a new subscription.
*
* @type int $user_id ID of the user to create the subscription for. Required.
* @type int $membership_level_id ID of the membership level to create the subscription for. Required.
* @type string $gateway Gateway to create the subscription for. Required.
* @type string $gateway_environment Gateway environment to create the subscription for. Required.
* @type string $subscription_transaction_id Subscription transaction ID to create the subscription for. Required.
* @type string $status Status of the subscription.
* @type string $startdate The subscription start date (UTC YYYY-MM-DD HH:MM:SS).
* @type string $enddate The subscription end date (UTC YYYY-MM-DD HH:MM:SS).
* @type string $next_payment_date The subscription next payment date (UTC YYYY-MM-DD HH:MM:SS).
* @type float $billing_amount The subscription billing amount.
* @type int $cycle_number The subscription cycle number.
* @type string $cycle_period The subscription cycle period.
* @type int $billing_limit The subscription billing limit.
* @type float $trial_amount The subscription trial amount.
* @type int $trial_limit The subscription trial limit.
* }
*
* @return PMPro_Subscription|null PMPro_Subscription object if created, null if not.
*/
public static function create( $args ) {
global $wpdb;
// Make sure that $args is an array.
$subscription_data = array();
if ( is_array( $args ) ) {
$subscription_data = $args;
} elseif ( is_object( $args ) ) {
$subscription_data = get_object_vars( $args );
} else {
// Invalid $subscription so there's nothing we can do.
return null;
}
// At a minimum, we need a user_id, membership_level_id, subscription_transaction_id, gateway, and gateway_environment.
if (
empty( $subscription_data['user_id'] ) ||
empty( $subscription_data['membership_level_id'] ) ||
empty( $subscription_data['subscription_transaction_id'] ) ||
empty( $subscription_data['gateway'] ) ||
empty( $subscription_data['gateway_environment'] )
) {
return null;
}
// Make sure we don't already have a subscription with this transaction ID and gateway.
$existing_subscription = self::get_subscription_from_subscription_transaction_id( $subscription_data['subscription_transaction_id'], $subscription_data['gateway'], $subscription_data['gateway_environment'] );
if ( ! empty( $existing_subscription ) ) {
// Subscription already exists.
return null;
}
// Create the subscription.
$new_subscription = new PMPro_Subscription( $subscription_data );
// Save the subscription before syncing with gateway
// to avoid infinite loops if gateways load orders which
// in turn try to create this subscription again.
$saved = $new_subscription->save();
if ( ! $saved ) {
// We couldn't save the subscription.
return null;
}
// Try to pull as much info as possible directly from the gateway or from the database.
$new_subscription->update();
return $new_subscription;
}
/**
* Update the startdate and next payment date based on information
* in the database, then sync with gateway.
*
* @since 3.0
*
* @return bool True if the subscription was saved, false if not.
*/
public function update() {
// Get the gateway object.
$gateway_object = $this->get_gateway_object();
// Track update errors.
$error_message = null;
// Make sure that the gateway object is valid.
if ( $gateway_object instanceof PMProGateway ) {
// Update subscription info.
$error_message = $gateway_object->update_subscription_info( $this );
} else {
$error_message = __( 'Could not find gateway class.', 'paid-memberships-pro' );
}
// Save error in subscription meta with date to reference later.
if ( ! empty( $error_message ) ) {
update_pmpro_subscription_meta( $this->id, 'sync_error', $error_message );
update_pmpro_subscription_meta( $this->id, 'sync_error_timestamp', current_time( 'timestamp' ) );
} else {
// No error, so clear the error meta.
delete_pmpro_subscription_meta( $this->id, 'sync_error' );
delete_pmpro_subscription_meta( $this->id, 'sync_error_timestamp' );
}
pmpro_setMessage( __( 'Subscription updated.', 'paid-memberships-pro' ), 'pmpro_success' ); // Will not overwrite previous messages.
return $this->save();
}
/**
* Update a subscription when a recurring payment is made. Should only
* be used on `pmpro_subscription_payment_completed` hook.
*
* @since 3.0
*
* @param MemberOrder $order The order for the recurring payment that was just processed.
*/
public static function update_subscription_for_order( $order ) {
$subscription = $order->get_subscription();
if ( ! empty( $subscription ) ) {
$subscription->update();
}
}
/**
* Get the next payment date for this subscription.
*
* @since 3.0
*
* @param string $format Format to return the date in.
* @param bool $local_time Whether to return the date in local time or UTC.
*
* @return string|null Date in the requested format.
*/
public function get_next_payment_date( $format = 'timestamp', $local_time = true ) {
return $this->format_subscription_date( $this->next_payment_date, $format, $local_time );
}
/**
* Get the start date for this subscription.
*
* @since 3.0
*
* @param string $format Format to return the date in.
* @param bool $local_time Whether to return the date in local time or UTC.
*
* @return string|null Date in the requested format.
*/
public function get_startdate( $format = 'timestamp', $local_time = true ) {
return $this->format_subscription_date( $this->startdate, $format, $local_time );
}
/**
* Get the end date for this subscription.
*
* @since 3.0
*
* @param string $format Format to return the date in.
* @param bool $local_time Whether to return the date in local time or UTC.
*
* @return string|null Date in the requested format.
*/
public function get_enddate( $format = 'timestamp', $local_time = true ) {
return $this->format_subscription_date( $this->enddate, $format, $local_time );
}
/**
* Format a date.
*
* @since 3.0
*
* @param string $date Date to format.
* @param string $format Format to return the date in.
* @param bool $local_time Whether to return the date in local time or UTC.
*
* @return string|null Date in the requested format.
*/
private function format_subscription_date( $date, $format = 'timestamp', $local_time = true ) {
if ( empty( $date ) || $date == '0000-00-00 00:00:00' ) {
return null;
}
if ( 'timestamp' === $format ) {
$format = 'U';
} elseif ( 'date_format' === $format ) {
$format = get_option( 'date_format' );
}
return wp_date( $format, strtotime( $date ), $local_time ? null : new DateTimezone( 'UTC' ) );
}
/**
* Returns the PMProGateway object for this subscription.
*
* @since 3.0
*
* @return null|PMProGateway The PMProGateway object, null if not set or class found.
*/
public function get_gateway_object() {
$gateway_object = null;
// The gateway was set.
if ( ! empty( $this->gateway ) ) {
// Default test gateway.
$classname = 'PMProGateway';
if ( 'free' !== $this->gateway ) {
// Adding the gateway suffix.
$classname .= '_' . $this->gateway;
}
if ( class_exists( $classname ) ) {
$gateway_object = new $classname( $this->gateway );
}
}
/**
* Allow changing the gateway object for this subscription
*
* @param PMProGateway $gateway_object Gateway object.
* @param PMPro_Subscription $this Subscription object.
*
* @since 3.0.3
*/
$gateway_object = apply_filters( 'pmpro_subscription_gateway_object', $gateway_object, $this );
return $gateway_object;
}
/**
* Get the initial payment amount for the subscription.
*
* @since 3.0
*
* @return float The initial payment amount for the subscription.
*/
public function get_initial_payment() {
if ( null !== $this->initial_payment ) {
return $this->initial_payment;
}
$this->initial_payment = 0;
// Fetch the first order for this subscription.
$orders = $this->get_orders( [
'limit' => 1,
'orderby' => '`timestamp` ASC, `id` ASC',
] );
if ( ! empty( $orders ) ) {
// Get the first order object.
$order = current( $orders );
// Use the order total as the initial payment.
$this->initial_payment = $order->total;
}
return $this->initial_payment;
}
/**
* Get the list of order objects for this subscription based on query arguments.
*
* Defaults to returning the latest 100 orders from a subscription based on the subscription transaction ID,
* the gateway, and the gateway environment.
*
* @since 3.0
*
* @param array $args The query arguments to use.
*
* @return MemberOrder[] The list of order objects.
*/
public function get_orders( array $args = [] ) {
if ( empty( $this->subscription_transaction_id ) ) {
return [];
}
global $wpdb;
$sql_query = "SELECT `id` FROM `$wpdb->pmpro_membership_orders`";
$prepared = [];
$where = [];
$orderby = isset( $args['orderby'] ) ? $args['orderby'] : '`timestamp` DESC, `id` DESC';
$limit = isset( $args['limit'] ) ? (int) $args['limit'] : 100;
// Detect unsupported orderby usage (in the future we may support better syntax).
if ( $orderby !== preg_replace( '/[^a-zA-Z0-9\s,`]/', ' ', $orderby ) ) {
return [];
}
// Filter by subscription transaction ID.
$where[] = 'subscription_transaction_id = %s';
$prepared[] = $this->subscription_transaction_id;
// Filter by gateway.
$where[] = 'gateway = %s';
$prepared[] = $this->gateway;
// Filter by gateway environment.
$where[] = 'gateway_environment = %s';
$prepared[] = $this->gateway_environment;
/*
* Now filter the query based on the arguments provided.
*/
// Filter by ID(s).
if ( isset( $args['id'] ) ) {
if ( ! is_array( $args['id'] ) ) {
$where[] = 'id = %d';
$prepared[] = $args['id'];
} else {
$where[] = 'id IN ( ' . implode( ', ', array_fill( 0, count( $args['id'] ), '%d' ) ) . ' )';
$prepared = array_merge( $prepared, $args['id'] );
}
}
// Filter by code(s).
if ( isset( $args['code'] ) ) {
if ( ! is_array( $args['code'] ) ) {
$where[] = 'code = %s';
$prepared[] = $args['code'];
} else {
$where[] = 'code IN ( ' . implode( ', ', array_fill( 0, count( $args['code'] ), '%s' ) ) . ' )';
$prepared = array_merge( $prepared, $args['code'] );
}
}
// Filter by status(es).
if ( isset( $args['status'] ) ) {
if ( ! is_array( $args['status'] ) ) {
$where[] = 'status = %s';
$prepared[] = $args['status'];
} else {
$where[] = 'status IN ( ' . implode( ', ', array_fill( 0, count( $args['status'] ), '%s' ) ) . ' )';
$prepared = array_merge( $prepared, $args['status'] );
}
}
// Filter by payment transaction ID(s).
if ( isset( $args['payment_transaction_id'] ) ) {
if ( ! is_array( $args['payment_transaction_id'] ) ) {
$where[] = 'payment_transaction_id = %s';
$prepared[] = $args['payment_transaction_id'];
} else {
$where[] = 'payment_transaction_id IN ( ' . implode( ', ', array_fill( 0, count( $args['payment_transaction_id'] ), '%s' ) ) . ' )';
$prepared = array_merge( $prepared, $args['payment_transaction_id'] );
}
}
// Maybe filter the data.
if ( $where ) {
$sql_query .= ' WHERE ' . implode( ' AND ', $where );
}
// Handle the order of data.
$sql_query .= ' ORDER BY ' . $orderby;
// Maybe limit the data.
if ( $limit ) {
$sql_query .= ' LIMIT %d';
$prepared[] = $limit;
}
// Maybe prepare the query.
if ( $prepared ) {
$sql_query = $wpdb->prepare( $sql_query, $prepared );
}
$order_ids = $wpdb->get_col( $sql_query );
if ( empty( $order_ids ) ) {
return [];
}
$orders = [];
foreach ( $order_ids as $order_id ) {
$order = new MemberOrder( $order_id );
// Make sure the order object is valid.
if ( ! empty( $order->id ) ) {
$orders[] = $order;
}
}
return $orders;
}
/**
* Get the cost text for this subscription.
*
* @since 3.0
*
* @return string
*/
public function get_cost_text() {
if ( 1 == $this->cycle_number ) {
// translators: %1$s - price, %2$s - period.
$cost_text = sprintf( __( '%1$s per %2$s', 'paid-memberships-pro' ), pmpro_formatPrice( $this->billing_amount ), pmpro_translate_billing_period( $this->cycle_period, $this->cycle_number ) );
} else {
// translators: %1$s - price, %2$d - number, %3$s - period.
$cost_text = sprintf( __( '%1$s every %2$d %3$s', 'paid-memberships-pro' ), pmpro_formatPrice( $this->billing_amount ), $this->cycle_number, pmpro_translate_billing_period( $this->cycle_period, $this->cycle_number ) );
}
/**
* Filter the cost text for this subscription.
*
* @since 3.1
*
* @param string $cost_text The cost text for this subscription.
* @param PMPro_Subscription $this The subscription object.
*/
return apply_filters( 'pmpro_subscription_cost_text', $cost_text, $this );
}
/**
* Set a property for this subscription.
*
* @since 3.0
*
* @param string|array $property Property to set, or an array with property => value pairs.
* @param mixed $value Value to set.
*/
public function set( $property, $value = null ) {
// Check if we need to set multiple properties as an array.
if ( is_array( $property ) ) {
foreach ( $property as $key => $value ) {
$this->set( $key, $value );
}
return;
}
// Perform validation as needed here.
if ( isset( $this->{$property} ) ) {
if ( is_int( $this->{$property} ) ) {
$value = (int) $value;
} elseif ( is_float( $this->{$property} ) ) {
$value = (float) $value;
}
}
$this->{$property} = $value;
}
/**
* Save the subscription using the current properties set. This will also set $subscription->id on creation.
*
* @since 3.0
*
* @return bool The new subscription ID or false if the save did not complete.
*/
public function save() {
global $wpdb;
// Handle required fields.
if ( empty( $this->gateway ) || empty( $this->gateway_environment ) || empty( $this->subscription_transaction_id ) ) {
return false;
}
// Active subscriptions shouldn't have an enddate yet, and cancelled subscriptions shouldn't have a next payment date.
if ( 'active' === $this->status ) {
$this->enddate = '';
} elseif ( 'cancelled' === $this->status ) {
$this->next_payment_date = '';
}
// If the startdate is empty or later than the current time, set it to the current time.
if ( empty( $this->startdate ) || strtotime( $this->startdate ) > time() ) {
$this->startdate = gmdate( 'Y-m-d H:i:s' );
}
// If the enddate is empty and the subscription is cancelled, set it to the current time.
if ( ( empty( $this->enddate ) || '0000-00-00 00:00:00' === $this->enddate || '1970-01-01 00:00:00' === $this->enddate ) && 'cancelled' === $this->status ) {
$this->enddate = gmdate( 'Y-m-d H:i:s' );
}
pmpro_insert_or_replace(
$wpdb->pmpro_subscriptions,
array(
'id' => $this->id,
'user_id' => $this->user_id,
'membership_level_id' => $this->membership_level_id,
'gateway' => $this->gateway,
'gateway_environment' => $this->gateway_environment,
'subscription_transaction_id' => $this->subscription_transaction_id,
'status' => $this->status,
'startdate' => $this->startdate,
'enddate' => $this->enddate,
'next_payment_date' => $this->next_payment_date,
'billing_amount' => $this->billing_amount,
'cycle_number' => $this->cycle_number,
'cycle_period' => $this->cycle_period,
'billing_limit' => $this->billing_limit,
'trial_amount' => $this->trial_amount,
'trial_limit' => $this->trial_limit,
),
array(
'%d', // id
'%d', // user_id
'%d', // membership_level_id
'%s', // gateway
'%s', // gateway_environment
'%s', // subscription_transaction_id
'%s', // status
'%s', // startdate
'%s', // enddate
'%s', // next_payment_date
'%f', // billing_amount
'%d', // cycle_number
'%s', // cycle_period
'%d', // billing_limit
'%f', // trial_amount
'%d', // trial_limit
),
'id'
);
if ( $wpdb->insert_id ) {
$this->id = $wpdb->insert_id;
/**
* Runs when a subscription is added.
*
* @param $this PMPro_Subscription The current subscription object
*
* @since 3.5
*/
do_action('pmpro_added_subscription', $this);
} else {
/**
* Runs when a subscription is updated.
*
* @param $this PMPro_Subscription The current subscription object
*
* @since 3.5
*/
do_action('pmpro_updated_subscription', $this);
}
// The subscription was not created properly.
if ( empty( $this->id ) ) {
pmpro_setMessage( __( 'There was an error saving the subscription.', 'paid-memberships-pro' ), 'pmpro_error' );
return false;
}
// If the subscription was cancelled, mark any incomplete orders as error.
if ( 'cancelled' === $this->status ) {
// Mark incomplete orders as error since the subscription is no longer active.
$incomplete_orders = $this->get_orders( array( 'status' => array( 'token', 'pending', 'review' ) ) );
if ( ! empty( $incomplete_orders ) ) {
foreach ( $incomplete_orders as $order ) {
$order->updateStatus( 'error' );
}
}
}
pmpro_setMessage( __( 'Subscription saved.', 'paid-memberships-pro' ), 'pmpro_success' ); // Will not overwrite previous messages.
return $this->id;
}
/**
* Cancels the subscription at the payment gateway.
*
* Legacy: Falls back on calling the gateway's cancel() method if the gateway does not
* support cancelling PMPro_Subscription objects specifically.
*
* @since 3.0
*
* @return bool True if the subscription was canceled successfully in the payment gateway.
*/
public function cancel_at_gateway() {
// Legacy: Prevent infinite loops when calling gateway's cancel() method and passing an order.
static $cancelled_subscription_ids = [];
if ( 'cancelled' !== $this->status && in_array( $this->id, $cancelled_subscription_ids, true ) ) {
return false;
}
$cancelled_subscription_ids[] = $this->id;
/**
* Mark the subscription as cancelled in the database before
* cancelling in payment gateway. We want this subscription
* to be cancelled in the database when the IPN/webhook hits
* so that the user's membership is not cancelled.
*/
$this->status = 'cancelled';
$this->save();
// Cancel the subscription in the gateway.
$cancelled = false;
$gateway_object = $this->get_gateway_object();
if ( is_object( $gateway_object ) ) {
/**
* Note here. We want to check if the gateway class
* _overrides_ the cancel_subscription method.
* So we use our new pmpro_method_defined_in_class() function.
* If not, we just look for a cancel method and fallback to cancelling that way.
* For that method_exists check, we are okay if the cancel method is in
* the extended class or the base class.
*/
if ( pmpro_method_defined_in_class( $gateway_object, 'cancel_subscription' ) ) {
$cancelled = $gateway_object->cancel_subscription( $this );
} elseif ( method_exists( $gateway_object, 'cancel' ) ) {
// Legacy: Build an order to pass to the old cancel() methods in gateways.
$morder = new MemberOrder();
$morder->user_id = $this->user_id;
$morder->membership_id = $this->membership_level_id;
$morder->gateway = $this->gateway;
$morder->gateway_environment = $this->gateway_environment;
$morder->subscription_transaction_id = $this->subscription_transaction_id;
$cancelled = $gateway_object->cancel( $morder );
}
}
// If the cancellation failed, send an email to the admin.
if ( ! $cancelled ) {
// Notify the admin.
$user = get_userdata( $this->user_id );
$pmproemail = new PMProEmail();
$pmproemail->template = 'subscription_cancel_error';
$pmproemail->data = array( 'body' => '<p>' . esc_html__( 'There was an error cancelling a subscription from your website. Check your payment gateway to see if the subscription is still active.', 'paid-memberships-pro' ) . '</p>' . "\n" );
$pmproemail->data['body'] .= '<p>' . esc_html__( 'User Email', 'paid-memberships-pro' ) . ': ' . $user->user_email . '</p>' . "\n";
$pmproemail->data['body'] .= '<p>' . esc_html__( 'Username', 'paid-memberships-pro' ) . ': ' . $user->user_login . '</p>' . "\n";
$pmproemail->data['body'] .= '<p>' . esc_html__( 'User Display Name', 'paid-memberships-pro' ) . ': ' . $user->display_name . '</p>' . "\n";
$pmproemail->data['body'] .= '<p>' . esc_html__( 'Subscription', 'paid-memberships-pro' ) . ': ' . $this->id . '</p>' . "\n";
$pmproemail->data['body'] .= '<p>' . esc_html__( 'Gateway', 'paid-memberships-pro' ) . ': ' . $this->gateway . '</p>' . "\n";
$pmproemail->data['body'] .= '<p>' . esc_html__( 'Subscription Transaction ID', 'paid-memberships-pro' ) . ': ' . $this->subscription_transaction_id . '</p>' . "\n";
$pmproemail->data['body'] .= '<hr />' . "\n";
$pmproemail->data['body'] .= '<p>' . esc_html__( 'Edit Member', 'paid-memberships-pro' ) . ': ' . esc_url( add_query_arg( array( 'page' => 'pmpro-member', 'user_id' => $this->user_id ), self_admin_url( 'admin.php' ) ) ) . '</p>';
$pmproemail->sendEmail( get_bloginfo( 'admin_email' ) );
pmpro_setMessage( __( 'There was an error cancelling a subscription from your website. Check your payment gateway to see if the subscription is still active.', 'paid-memberships-pro' ), 'pmpro_error', true ); // Will overwrite previous messages.
} else {
pmpro_setMessage( __( 'Subscription cancelled at gateway.', 'paid-memberships-pro' ), 'pmpro_success', true ); // Will overwrite previous messages.
}
$this->update();
return $cancelled;
}
/**
* Checks if this subscription has default migration data and,
* if so, fixes it.
*
* @since 3.0
*/
private function maybe_fix_default_migration_data() {
// Make sure that this looks like default migration data for an active subscription.
if ( empty( $this->id ) || ! empty( $this->billing_amount ) || ! empty( $this->cycle_number ) ) {
// This is not default migration data for an active subscription. Bail.
return;
}
/**
* Filter to skip fixing default migration data for a subscription.
*
* Useful in cases such as CSV imports where we need to be performant when creating subscriptions.
* In such a use-case, the following steps should be taken:
* 1. Use this hook to disable updating default migration data.
* 2. Directly add the subscription data to the database with "default migration data".
* 3. Create any orders for the subscription (this should only be done after the subscription is created in the db).
* 4. After all entries are processed, add the `pmpro_upgrade_3_0_ajax` update so that admins can automatically sync the subscriptions after the import is complete.
*
* @since 3.0
*
* @param bool $skip_fixing_default_migration_data True to skip fixing default migration data for a subscription, false to process it.
*/
$skip_fixing_default_migration_data = apply_filters( 'pmpro_subscription_skip_fixing_default_migration_data', false );
if ( $skip_fixing_default_migration_data ) {
return;
}
/*
* The following data should already be correct from the migration:
* id, user_id, membership_level_id, gateway, gateway_environment, subscription_transaction_id, status.
*
* In order to populate the rest of the subscription data, we need to guess at the
* biling_amount, cycle_number, cycle_period, billing_limit, trial_amount, and trial_limit.
*
* Our approach for guessing will be as follows:
* 1. Get all previous membership levels for the user (including old membesrhips). Can't use
* pmpro_get_specific_membership_levels_for_user() because it doesn't include
* old memberships.
* 2. Loop through membership levels in reverse (most recent first).
* 3. If we find a membership level that matches the subscription level and is recurring,
* then let's assume that this is the membership level that the subscription was
* created for.
* 4. If we do not find a membership level that matches the subscription level and is recurring,
* then let's use the default membership level settings if it is recurring.
*/
if ( 'active' === $this->status ) { // Only guess for active subscriptions. For cancelled subscriptions, we would rather show $0/month than a potentially wrong amount.
$all_user_levels = pmpro_getMembershipLevelsForUser( $this->user_id, true ); // True to include old memberships.
// Looping through $all_user_levels backwards to get the most recent first.
for ( end( $all_user_levels ); key( $all_user_levels ) !== null; prev( $all_user_levels ) ) {
$level_check = current( $all_user_levels );
// Let's check if level the same level as this subscription and if it's a recurring level.
if ( $level_check->id == $this->membership_level_id && ! empty( $level_check->billing_amount ) && ! empty( $level_check->cycle_number ) ) {
$subscription_level = $level_check;
break;
}
}
}
// If the user hasn't had a recurring membership for this level, pull from the level settings instead.
// Only do this if the subscription is active since we can guess wrong and may not be able to sync with the gateway since this is an old subscription.
if ( empty( $subscription_level ) && 'active' === $this->status) {
$level = pmpro_getLevel( $this->membership_level_id );
if ( ! empty( $level ) && ! empty( $level->billing_amount ) && ! empty( $level->cycle_number ) ) {
$subscription_level = $level;
}
}
// If we still don't have a subscription level, then this membership level isn't recurring or no longer exists on the site.
// Give it some default values.
if ( empty( $subscription_level ) ) {
$subscription_level = new stdClass();
$subscription_level->billing_amount = 0;
$subscription_level->cycle_number = 1;
$subscription_level->cycle_period = 'Month';
$subscription_level->billing_limit = 0;
$subscription_level->trial_amount = 0;
$subscription_level->trial_limit = 0;
}
// We have found a level, let's fill in the subscription.
$this->billing_amount = $subscription_level->billing_amount;
$this->cycle_number = $subscription_level->cycle_number;
$this->cycle_period = $subscription_level->cycle_period;
$this->billing_limit = $subscription_level->billing_limit;
$this->trial_amount = $subscription_level->trial_amount;
$this->trial_limit = $subscription_level->trial_limit;
// Save so that we don't start another migration when we call get_orders().
$this->save();
// Let's take a guess at the start date.
$oldest_orders = $this->get_orders( [
'limit' => 1,
'orderby' => '`timestamp` ASC, `id` ASC',
] );
if ( ! empty( $oldest_orders ) ) {
$oldest_order = current( $oldest_orders );
$this->startdate = date_i18n( 'Y-m-d H:i:s', $oldest_order->getTimestamp( true ) );
}
// Let's also take a guess at the next payment date or end date.
$newest_orders = $this->get_orders(
[
'limit' => 1,
'orderby' => '`timestamp` DESC, `id` DESC',
]
);
if ( ! empty( $newest_orders ) ) {
$newest_order = current( $newest_orders );
if ( 'active' === $this->status ) {
$this->next_payment_date = date_i18n( 'Y-m-d H:i:s', strtotime( '+ ' . $this->cycle_number . ' ' . $this->cycle_period, $newest_order->getTimestamp( true ) ) );
} else {
$this->enddate = date_i18n( 'Y-m-d H:i:s', $newest_order->getTimestamp( true ) );
}
}
// Now that we have the basic data filled in, the `update()` method will take care of the rest.
$this->update();
}
/**
* Check if the billing limit has been reached for this subscription.
*
* @since 3.0
*
* @return bool False if there is not a billing limit or if the billing limit has not yet been reached. True otherwise.
*/
public function billing_limit_reached() {
// If there is no billing limit, then we can't have reached it.
if ( empty( $this->billing_limit ) ) {
return false;
}
// Billing limits do not include the initial order.
// With this in mind, get the last [billing_limit+1] successful orders for this subscription.
$orders_args = array(
'limit' => $this->billing_limit + 1,
'status' => 'success',
);
$orders = $this->get_orders( $orders_args );
// Check if we have reached the billing limit.
return count( $orders ) >= $this->billing_limit + 1;
}
} // end of class
// @todo Move this into another location outside of the bottom of the class file.
// Update the subscription status when a recurring payment is successful.
// Cancellations during IPNs/Webhooks are handled when the order is "changed" to 'cancelled' status, which is passed through to the sub.
add_action( 'pmpro_subscription_payment_completed', [ 'PMPro_Subscription', 'update_subscription_for_order' ] );