Files
wp-multisite-waas/inc/models/class-membership.php
2025-02-09 12:30:02 -07:00

2528 lines
55 KiB
PHP

<?php
/**
* The Membership model.
*
* @package WP_Ultimo
* @subpackage Models
* @since 2.0.0
*/
namespace WP_Ultimo\Models;
use WP_Ultimo\Models\Base_Model;
use WP_Ultimo\Models\Product;
use WP_Ultimo\Models\Site;
use WP_Ultimo\Database\Memberships\Membership_Status;
use WP_Ultimo\Checkout\Cart;
// Exit if accessed directly
defined('ABSPATH') || exit;
/**
* Membership model class. Implements the Base Model.
*
* @since 2.0.0
*/
class Membership extends Base_Model {
use Traits\Limitable;
use Traits\Billable;
use Traits\Notable;
use \WP_Ultimo\Traits\WP_Ultimo_Subscription_Deprecated;
/**
* ID of the customer attached to this membership.
*
* @since 2.0.0
* @var int
*/
protected $customer_id;
/**
* User ID attached to this membership.
*
* @since 2.0.0
* @var int
*/
protected $user_id;
/**
* Plan associated with the membership.
*
* @since 2.0.0
* @var int
*/
protected $plan_id;
/**
* Additional products. Services and Packages.
*
* @since 2.0.0
* @var array
*/
protected $addon_products = [];
/**
* Currency for this membership. 3-letter currency code.
*
* @since 2.0.0
* @var string
*/
protected $currency;
/**
* Initial amount for the subscription. Includes the setup fee.
*
* @since 2.0.0
* @var float
*/
protected $initial_amount = 0;
/**
* Is this product recurring?
*
* @since 2.0.0
* @var int|boolean
*/
protected $recurring = 1;
/**
* Should auto-renew?
*
* @since 2.0.0
* @var int|boolean
*/
protected $auto_renew = 0;
/**
* Time interval between charges.
*
* @since 2.0.0
* @var int
*/
protected $duration = 1;
/**
* Time interval unit between charges.
*
* - day
* - week
* - month
* - year
*
* @since 2.0.0
* @var string
*/
protected $duration_unit = 'month';
/**
* Amount to charge recurrently.
*
* @since 2.0.0
* @var float
*/
protected $amount = 0;
/**
* Date of creation of this membership.
*
* @since 2.0.0
* @var string
*/
protected $date_created;
/**
* Date of activation of this membership.
*
* @since 2.0.0
* @var string
*/
protected $date_activated;
/**
* Date of the end of the trial period.
*
* @since 2.0.0
* @var string
*/
protected $date_trial_end;
/**
* Date of the next renewal.
*
* @since 2.0.0
* @var string
*/
protected $date_renewed;
/**
* Date of the cancellation.
*
* @since 2.0.0
* @var string
*/
protected $date_cancellation;
/**
* Date of expiration.
*
* @since 2.0.0
* @var string
*/
protected $date_expiration;
/**
* Change of the payment completion for the plan value.
*
* @since 2.0.0
* @var string
*/
protected $date_payment_plan_completed;
/**
* Amount of times this membership got billed.
*
* @since 2.0.0
* @var int
*/
protected $times_billed = 0;
/**
* Maximum times we should charge this membership.
*
* @since 2.0.0
* @var int
*/
protected $billing_cycles;
/**
* Status of the membership.
*
* @since 2.0.0
* @var string
*/
protected $status;
/**
* ID of the customer on the payment gateway database.
*
* @since 2.0.0
* @var string
*/
protected $gateway_customer_id;
/**
* ID of the subscription on the payment gateway database.
*
* @since 2.0.0
* @var string
*/
protected $gateway_subscription_id;
/**
* ID of the gateway being used on this subscription.
*
* @since 2.0.0
* @var string
*/
protected $gateway;
/**
* Signup method used to create this membership.
*
* @since 2.0.0
* @var string
*/
protected $signup_method;
/**
* Plan that this membership upgraded from.
*
* @since 2.0.0
* @var int
*/
protected $upgraded_from;
/**
* Date this membership was last modified.
*
* @since 2.0.0
* @var string
*/
protected $date_modified;
/**
* If this membership is disabled.
*
* @since 2.0.0
* @var bool
*/
protected $disabled;
/**
* The discount code applied to this membership
*
* @since 2.0.20
* @var \WP_Ultimo\Models\Discount_Code
*/
protected $discount_code;
/**
* The reason for the cancellation of this membership
*
* @since 2.1.2
* @var string
*/
protected $cancellation_reason;
/**
* Keep original list of products.
*
* If the products are changed for some reason,
* we need to run additional code to handle updates
* in other parts of the code.
*
* For example, when products change, we
* might need to change the user role in every
* sub-site belonging to that membership.
*
* @since 2.0.10
* @var array
*/
protected $_compiled_product_list = [];
/**
* Keep original gateway info.
*
* If some gateway info change for some reason,
* we need to run additional code to handle updates
* in other parts of the code (eg: Cancel other gateways).
*
* @since 2.0.15
* @var array
*/
protected $_gateway_info = [];
/**
* Query Class to the static query methods.
*
* @since 2.0.0
* @var string
*/
protected $query_class = \WP_Ultimo\Database\Memberships\Membership_Query::class;
/**
* Constructs the object via the constructor arguments
*
* @since 2.0.0
*
* @param object $object Std object with model parameters.
*/
public function __construct($object = null) {
parent::__construct($object);
$this->_gateway_info = [
'gateway' => $this->get_gateway(),
'gateway_customer_id' => $this->get_gateway_customer_id(),
'gateway_subscription_id' => $this->get_gateway_subscription_id(),
];
if (did_action('plugins_loaded')) {
$this->_compiled_product_list = $this->get_all_products();
}
}
/**
* Set the validation rules for this particular model.
*
* To see how to setup rules, check the documentation of the
* validation library we are using: https://github.com/rakit/validation
*
* @since 2.0.0
* @link https://github.com/rakit/validation
* @return array
*/
public function validation_rules() {
$currency = wu_get_setting('currency_symbol', 'USD');
$membership_status = new \WP_Ultimo\Database\Memberships\Membership_Status();
$membership_status = $membership_status->get_allowed_list(true);
return [
'customer_id' => 'required|integer|exists:\WP_Ultimo\Models\Customer,id',
'user_id' => 'integer',
'plan_id' => 'required|integer|exists:\WP_Ultimo\Models\Product,id',
'currency' => "default:{$currency}",
'duration' => 'numeric|default:1',
'duration_unit' => 'in:day,week,month,year',
'initial_amount' => 'numeric',
'auto_renew' => 'boolean|default:1',
'status' => "in:{$membership_status}|default:pending",
'gateway_customer_id' => 'default:',
'upgraded_from' => 'default:',
'amount' => 'numeric|default:0',
'billing_cycles' => 'numeric|default:0',
'times_billed' => 'integer|default:0',
'gateway' => 'default:',
'signup_method' => 'default:',
'disabled' => 'default:0',
'recurring' => 'default:0',
];
}
/**
* Gets the customer object associated with this membership.
*
* @todo Implement this.
* @since 2.0.0
* @return \WP_Ultimo\Models\Customer;
*/
public function get_customer() {
return wu_get_customer($this->get_customer_id());
}
/**
* Get the value of customer_id.
*
* @since 2.0.0
* @return int
*/
public function get_customer_id() {
return absint($this->customer_id);
}
/**
* Set the value of customer_id.
*
* @since 2.0.0
* @param int $customer_id The ID of the customer attached to this membership.
* @return void
*/
public function set_customer_id($customer_id): void {
$this->customer_id = absint($customer_id);
}
/**
* Checks if a given customer should have access to this site options.
*
* @since 2.0.0
*
* @param int $customer_id The customer id to check.
* @return boolean
*/
public function is_customer_allowed($customer_id = false) {
if (current_user_can('manage_network')) {
return true;
}
if ( ! $customer_id) {
$customer = WP_Ultimo()->currents->get_customer();
$customer_id = $customer ? $customer->get_id() : 0;
}
$allowed = absint($customer_id) === absint($this->get_customer_id());
return apply_filters('wu_membership_is_customer_allowed', $allowed, $customer_id, $this);
}
/**
* Get the value of user_id.
*
* @since 2.0.0
* @return int
*/
public function get_user_id() {
return $this->user_id;
}
/**
* Set the value of user_id.
*
* @since 2.0.0
* @param int $user_id The user ID attached to this membership.
* @return void
*/
public function set_user_id($user_id): void {
$this->user_id = absint($user_id);
}
/**
* Get the value of plan_id.
*
* @since 2.0.0
* @return int
*/
public function get_plan_id() {
return (int) $this->plan_id;
}
/**
* Returns the plan that created this membership.
*
* @since 2.0.0
* @return \WP_Ultimo\Models\Product|false
*/
public function get_plan() {
$plan = wu_get_product($this->get_plan_id());
// Get the correct ariation if exists
if ($plan && ($plan->get_duration() !== $this->get_duration() || $plan->get_duration_unit() !== $this->get_duration_unit())) {
$variation = $plan->get_as_variation($this->get_duration(), $this->get_duration_unit());
$plan = ($variation ?: null) ?? $plan;
}
return $plan;
}
/**
* Set plan associated with the membership.
*
* @since 2.0.0
* @param int $plan_id The plan ID associated with the membership.
* @return void
*/
public function set_plan_id($plan_id): void {
$this->plan_id = absint($plan_id);
}
/**
* Checks if this membership has a plan.
*
* @since 2.0.0
* @return boolean
*/
public function has_plan() {
return ! empty($this->get_plan());
}
/**
* Get additional product objects.
*
* @since 2.0.0
* @return \WP_Ultimo\Models\Product[] A list of the addon projects.
*/
public function get_addons(): array {
$addons = array_map('wu_get_product', $this->get_addon_ids());
return array_filter($addons);
}
/**
* Checks if the given membership has addon products.
*
* @since 2.0.0
* @return boolean
*/
public function has_addons() {
return ! empty($this->get_addons());
}
/**
* Gets a list of product ids for addons.
*
* @since 2.0.0
*/
public function get_addon_ids(): array {
return array_map('absint', array_keys((array) $this->addon_products));
}
/**
* Adds an an addon product from this membership.
*
* @since 2.0.0
*
* @param int $product_id The product id.
* @param integer $quantity The quantity.
* @return void
*/
public function add_product($product_id, $quantity = 1): void {
$has_product = wu_get_isset($this->addon_products, $product_id);
if ($has_product && $this->addon_products[ $product_id ] >= 0) {
$this->addon_products[ $product_id ] += $quantity;
} else {
$this->addon_products[ $product_id ] = $quantity;
}
if ($this->addon_products[ $product_id ] <= 0) {
unset($this->addon_products[ $product_id ]);
}
}
/**
* Removes a product from the membership.
*
* @since 2.0.0
*
* @param integer $product_id The product id.
* @param integer $quantity The quantity to remove.
* @return void
*/
public function remove_product($product_id, $quantity = 1): void {
$has_product = wu_get_isset($this->addon_products, $product_id);
if ($has_product && $this->addon_products[ $product_id ] >= 0) {
$this->addon_products[ $product_id ] -= $quantity;
}
if ($this->addon_products[ $product_id ] <= 0) {
unset($this->addon_products[ $product_id ]);
}
}
/**
* Get additional products. Services and Packages.
*
* @since 2.0.0
* @return array
*/
public function get_addon_products() {
$products = [];
$this->addon_products = is_array($this->addon_products) ? $this->addon_products : [];
foreach ($this->addon_products as $product_id => $quantity) {
$product = wu_get_product($product_id);
if ( ! $product) {
continue;
}
$products[] = [
'quantity' => $quantity,
'product' => $product,
];
}
return $products;
}
/**
* Returns a list with all products, including the plan.
*
* @since 2.0.0
* @return array
*/
public function get_all_products() {
$products = [
[
'quantity' => 1,
'product' => $this->get_plan(),
],
];
return array_merge($products, $this->get_addon_products());
}
/**
* Set additional products. Services and Packages.
*
* @since 2.0.0
* @param mixed $addon_products Additional products related to this membership. Services, Packages or other types of products.
* @return void
*/
public function set_addon_products($addon_products): void {
$this->addon_products = maybe_unserialize($addon_products);
}
/**
* Changes the membership products and totals.
*
* This is used when a upgrade, downgrade or addon
* checkout is processed.
*
* It takes a Cart object and uses that to construct
* the new membership parameters.
*
* Important: this method does not SAVE the changes
* you need to explicitly call save() after a swap
* to persist the changes.
*
* @since 2.0.0
*
* @param Cart $order The cart object.
* @return \WP_Ultimo\Models\Membership
*/
public function swap($order) {
if ( ! is_a($order, Cart::class)) {
return new \WP_Error('invalid-date', __('Swap Cart is invalid.', 'wp-ultimo'));
}
// clear the current addons.
$this->addon_products = [];
/*
* We'll do that based on the line items,
* we do it that way because it is the only
* place where we have quantity info, as well
* as product ID.
*/
foreach ($order->get_line_items() as $line_item) {
$product = $line_item->get_product();
/**
* We only care about products.
*/
if (empty($product)) {
continue;
}
/*
* Checks if this is a plan.
*
* If that's the case, we need to replace the current
* plan id.
*/
if ($product->get_type() === 'plan') {
$this->set_plan_id($product->get_id());
continue;
}
/*
* For other products,
* we add them as addons.
*/
$this->add_product($product->get_id(), $line_item->get_quantity());
}
/*
* Finally, we have a couple of other parameters to set.
*/
$this->set_amount($order->get_recurring_total());
$this->set_initial_amount($order->get_total());
$this->set_recurring($order->has_recurring());
$this->set_duration($order->get_duration());
$this->set_duration_unit($order->get_duration_unit());
/*
* Returns self for chaining.
*/
return $this;
}
/**
* Schedule a swap for the membership.
*
* @since 2.0.0
*
* @param Cart $order The cart representing the change.
* @param string|boolean $schedule_date The date to schedule the change for.
* @return int|\WP_Error
*/
public function schedule_swap($order, $schedule_date = false) {
if (empty($schedule_date)) {
$schedule_date = $this->get_date_expiration();
}
if ( ! wu_validate_date($schedule_date)) {
return new \WP_Error('invalid-date', __('Schedule date is invalid.', 'wp-ultimo'));
}
if ( ! is_a($order, Cart::class)) {
return new \WP_Error('invalid-date', __('Swap Cart is invalid.', 'wp-ultimo'));
}
$date_instance = wu_date($schedule_date);
/*
* Saves the order.
*/
$this->update_meta('wu_swap_order', $order);
$this->update_meta('wu_swap_scheduled_date', $schedule_date);
/*
* Remove the previous swaps.
*/
wu_unschedule_action(
'wu_async_membership_swap',
[
'membership_id' => $this->get_id(),
],
'membership'
);
/*
* Schedule the swap.
*/
return wu_schedule_single_action(
$date_instance->format('U'),
'wu_async_membership_swap',
[
'membership_id' => $this->get_id(),
],
'membership'
);
}
/**
* Returns the scheduled swap, if any.
*
* @since 2.0.0
* @return object
*/
public function get_scheduled_swap() {
$order = $this->get_meta('wu_swap_order');
$scheduled_date = $this->get_meta('wu_swap_scheduled_date');
if ( ! $scheduled_date || ! $order) {
$this->delete_meta('wu_swap_order');
$this->delete_meta('wu_swap_scheduled_date');
return false;
}
return (object) [
'order' => $order,
'scheduled_date' => $scheduled_date,
];
}
/**
* Removes a schedule swap.
*
* @since 2.0.0
* @return void
*/
public function delete_scheduled_swap(): void {
$this->delete_meta('wu_swap_order');
$this->delete_meta('wu_swap_scheduled_date');
do_action('wu_membership_delete_scheduled_swap', $this);
}
/**
* Returns the amount recurring in a human-friendly way.
*
* @since 2.0.0
* @return string
*/
public function get_recurring_description() {
$description = sprintf(
// translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc)
_n('every %2$s', 'every %1$s %2$s', $this->get_duration(), 'wp-ultimo'), // phpcs:ignore
$this->get_duration(),
wu_get_translatable_string(($this->get_duration() <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's'))
);
return $description;
}
/**
* Returns the times billed in a human-friendly way.
*
* @since 2.0.0
*/
public function get_times_billed_description(): string {
// translators: times billed / subscription duration in cycles. e.g. 1/12 cycles
$description = __('%1$s / %2$s cycles', 'wp-ultimo');
if ($this->is_forever_recurring()) {
// translators: the place holder is the number of times the membership was billed.
$description = __('%1$s / until cancelled', 'wp-ultimo');
}
return sprintf($description, $this->get_times_billed(), $this->get_billing_cycles());
}
/**
* Returns the membership price structure in a way human can understand it.
*
* @since 2.0.0
*/
public function get_price_description(): string {
$pricing = [];
if ($this->is_recurring()) {
$duration = $this->get_duration();
$message = sprintf(
// translators: %1$s is the formatted price, %2$s the duration, and %3$s the duration unit (day, week, month, etc)
_n('%1$s every %3$s', '%1$s every %2$s %3$s', $duration, 'wp-ultimo'), // phpcs:ignore
wu_format_currency($this->get_amount(), $this->get_currency()),
$duration,
wu_get_translatable_string($duration <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's')
);
$pricing['subscription'] = $message;
if ( ! $this->is_forever_recurring()) {
$billing_cycles_message = sprintf(
// translators: %s is the number of billing cycles.
_n('for %s cycle', 'for %s cycles', $this->get_billing_cycles(), 'wp-ultimo'),
$this->get_billing_cycles()
);
$pricing['subscription'] .= ' ' . $billing_cycles_message;
}
} else {
$pricing['subscription'] = sprintf(
// translators: %1$s is the formatted price of the product
__('%1$s one time payment', 'wp-ultimo'),
wu_format_currency($this->get_initial_amount(), $this->get_currency())
);
}
if ($this->is_free()) {
$pricing['subscription'] = __('Free!', 'wp-ultimo');
}
return implode(' + ', $pricing);
}
/**
* Get the value of currency.
*
* @since 2.0.0
* @return string
*/
public function get_currency() {
// return $this->currency; For now, multi-currency is not yet supported.
return wu_get_setting('currency_symbol', 'USD');
}
/**
* Set the value of currency.
*
* @since 2.0.0
* @param string $currency The currency that this membership. It's a 3-letter code. E.g. 'USD'.
* @return void
*/
public function set_currency($currency): void {
$this->currency = $currency;
}
/**
* Get time interval between charges.
*
* @return int
*/
public function get_duration() {
return absint($this->duration);
}
/**
* Set time interval between charges.
*
* @param int $duration The interval period between a charge. Only the interval amount, the unit will be defined in another property.
*/
public function set_duration($duration): void {
$this->duration = absint($duration);
}
/**
* Get time interval unit between charges.
*
* @return string
*/
public function get_duration_unit() {
return $this->duration_unit;
}
/**
* Set time interval unit between charges.
*
* @param string $duration_unit The duration amount type. Can be 'day', 'week', 'month' or 'year'.
*/
public function set_duration_unit($duration_unit): void {
$this->duration_unit = $duration_unit;
}
/**
* Get the product amount.
*
* @return int
*/
public function get_amount() {
return $this->amount;
}
/**
* Get normalized amount. This is used to calculate MRR>
*
* @since 2.0.0
* @return float
*/
public function get_normalized_amount() {
$amount = $this->get_amount();
if ($this->is_recurring()) {
return $amount;
}
$duration = $this->get_duration();
$normalized_duration_unit = wu_convert_duration_unit_to_month($this->get_duration_unit());
return $amount / $duration * $normalized_duration_unit;
}
/**
* Set the product amount.
*
* @param float $amount The product amount.
*/
public function set_amount($amount): void {
$this->amount = wu_to_float($amount);
}
/**
* Get the product setup fee.
*
* @return int
*/
public function get_initial_amount() {
return $this->initial_amount;
}
/**
* Set the product setup fee.
*
* @param float $initial_amount The initial amount charged for this membership, including the setup fee.
*/
public function set_initial_amount($initial_amount): void {
$this->initial_amount = wu_to_float($initial_amount);
}
/**
* Get the value of date_created.
*
* @since 2.0.0
* @return string
*/
public function get_date_created() {
return $this->date_created;
}
/**
* Set the value of date_created.
*
* @since 2.0.0
* @param string $date_created Date of creation of this membership.
* @return void
*/
public function set_date_created($date_created): void {
$this->date_created = $date_created;
}
/**
* Get the value of date_activated.
*
* @since 2.0.0
* @return string
*/
public function get_date_activated() {
return $this->date_activated;
}
/**
* Set the value of date_activated.
*
* @since 2.0.0
* @param string $date_activated Date when this membership was activated.
* @return void
*/
public function set_date_activated($date_activated): void {
$this->date_activated = $date_activated;
}
/**
* Get the value of date_trial_end.
*
* @since 2.0.0
* @return string
*/
public function get_date_trial_end() {
return $this->date_trial_end;
}
/**
* Set the value of date_trial_end.
*
* @since 2.0.0
* @param string $date_trial_end Date when the trial period ends, if this membership has or had a trial period.
* @return void
*/
public function set_date_trial_end($date_trial_end): void {
$this->date_trial_end = $date_trial_end;
}
/**
* Get the value of date_renewed.
*
* @since 2.0.0
* @return string
*/
public function get_date_renewed() {
return $this->date_renewed;
}
/**
* Set the value of date_renewed.
*
* @since 2.0.0
* @param string $date_renewed Date when the membership was cancelled.
* @return void
*/
public function set_date_renewed($date_renewed): void {
$this->date_renewed = $date_renewed;
}
/**
* Get the value of date_cancellation.
*
* @since 2.0.0
* @return string
*/
public function get_date_cancellation() {
return $this->date_cancellation;
}
/**
* Set the value of date_cancellation.
*
* @since 2.0.0
* @param string $date_cancellation Date when the membership was cancelled.
* @return void
*/
public function set_date_cancellation($date_cancellation): void {
$this->date_cancellation = $date_cancellation;
}
/**
* Get the cancellation reason
*
* @since 2.1.2
* @return string
*/
public function get_cancellation_reason() {
if ($this->get_status() !== Membership_Status::CANCELLED) {
return '';
}
if (null === $this->cancellation_reason) {
$this->cancellation_reason = $this->get_meta('cancellation_reason');
}
return (string) $this->cancellation_reason;
}
/**
* Set the reason to cancel the membership.
*
* @since 2.1.2
* @param string $reason The reason to cancel the membership.
* @return void
*/
public function set_cancellation_reason($reason): void {
$this->meta['cancellation_reason'] = $reason;
$this->cancellation_reason = $reason;
}
/**
* Get the value of date_expiration.
*
* @since 2.0.0
* @return string
*/
public function get_date_expiration() {
return $this->date_expiration;
}
/**
* Set the value of date_expiration.
*
* @since 2.0.0
* @param string $date_expiration Date when the membership will expiry.
* @return void
*/
public function set_date_expiration($date_expiration): void {
$this->date_expiration = $date_expiration;
}
/**
* Calculate a new expiration date.
*
* @since 2.0.0
*
* @param bool $from_today Whether to calculate from today (`true`), or extend the existing expiration date (`false`).
* @param bool $trial Whether or not this is for a free trial.
* @return String Date in Y-m-d H:i:s format or null if is a lifetime membership.
*/
public function calculate_expiration($from_today = false, $trial = false) {
// Get the member's current expiration date
$expiration = $this->get_date_expiration();
$expiration_timestamp = 0;
if ( ! $this->is_recurring()) {
return null;
}
if (wu_validate_date($expiration)) {
$expiration_timestamp = wu_date($expiration)->format('U');
}
// Determine what date to use as the start for the new expiration calculation
if (!$from_today && $expiration_timestamp > wu_get_current_time('timestamp', true)) { // phpcs:ignore
$base_timestamp = $expiration_timestamp;
} else {
$base_timestamp = wu_get_current_time('timestamp', true); // phpcs:ignore
}
if ($this->get_duration() > 0) {
if (false && $this->trial_duration > 0 && $trial) {
$expire_timestamp = strtotime('+' . $this->get_trial_duration() . ' ' . $this->trial_duration_unit . ' 23:59:59', $base_timestamp);
} else {
$expire_timestamp = strtotime('+' . $this->get_duration() . ' ' . $this->get_duration_unit() . ' 23:59:59', $base_timestamp);
}
$extension_days = ['29', '30', '31'];
if (in_array(gmdate('j', $expire_timestamp), $extension_days, true) && 'month' === $this->get_duration_unit()) {
$month = gmdate('n', $expire_timestamp);
if ($month < 12) {
$month += 1;
$year = gmdate('Y', $expire_timestamp);
} else {
$month = 1;
$year = gmdate('Y', $expire_timestamp) + 1;
}
$expire_timestamp = mktime(0, 0, 0, $month, 1, $year);
}
$expiration = gmdate('Y-m-d 23:59:59', $expire_timestamp);
} else {
$expiration = null; // tag as lifetime.
}
/**
* Filters the calculated expiration date.
*
* @param string $expiration Calculated expiration date in MySQL format.
* @param int $membership_id ID of the membership.
* @param \WP_Ultimo\Models\Membership $this Membership object.
*
* @since 2.0
*/
$expiration = apply_filters('wu_membership_calculated_date_expiration', $expiration, $this->get_id(), $this);
return $expiration;
}
/**
* Get the value of date_payment_plan_completed.
*
* @since 2.0.0
* @return string
*/
public function get_date_payment_plan_completed() {
return $this->date_payment_plan_completed;
}
/**
* Set the value of date_payment_plan_completed.
*
* @since 2.0.0
* @param string $date_payment_plan_completed Change of the payment completion for the plan value.
* @return void
*/
public function set_date_payment_plan_completed($date_payment_plan_completed): void {
$this->date_payment_plan_completed = $date_payment_plan_completed;
}
/**
* Get the value of auto_renew.
*
* @since 2.0.0
* @return boolean
*/
public function should_auto_renew() {
return (bool) $this->auto_renew;
}
/**
* Deprecated: get_auto_renew
*
* @since 2.0.0
* @return boolean
*/
public function get_auto_renew() {
_deprecated_function(__METHOD__, '2.0.0', 'should_auto_renew()');
return $this->should_auto_renew();
}
/**
* Set the value of auto_renew.
*
* @since 2.0.0
* @param bool $auto_renew If this membership should auto-renewal.
* @return void
*/
public function set_auto_renew($auto_renew): void {
$this->auto_renew = (bool) $auto_renew;
}
/**
* Get the discount code applied if exist.
*
* @since 2.0.20
* @return \WP_Ultimo\Models\Discount_Code|false
*/
public function get_discount_code() {
if (null === $this->discount_code) {
$this->discount_code = $this->get_meta('discount_code');
}
// Get discount code from original payment for compatibility
if (empty($this->discount_code) && ! $this->get_meta('verified_payment_discount')) {
$original_payment = wu_get_payments(
[
'number' => 1,
'membership_id' => $this->get_id(),
'orderby' => 'id',
'order' => 'ASC',
]
);
if (isset($original_payment[0])) {
$original_cart = $original_payment[0]->get_meta('wu_original_cart');
$this->discount_code = $original_cart ? $original_cart->get_discount_code() : false;
if ($this->discount_code) {
$this->update_meta('discount_code', $this->discount_code);
}
}
$this->update_meta('verified_payment_discount', true);
}
return $this->discount_code;
}
/**
* Set the value of discount_code.
*
* @since 2.0.20
* @param string|WP_Ultimo\Models\Discount_Code $discount_code Discount code object.
* @return void
*/
public function set_discount_code($discount_code): void {
if (is_a($discount_code, '\WP_Ultimo\Models\Discount_Code')) {
$this->meta['discount_code'] = $discount_code;
$this->discount_code = $discount_code;
return;
}
$discount_code = wu_get_discount_code_by_code($discount_code);
if ( ! $discount_code) {
return;
}
$this->meta['discount_code'] = $discount_code;
$this->discount_code = $discount_code;
}
/**
* Get the value of times_billed.
*
* @since 2.0.0
* @return int
*/
public function get_times_billed() {
return (int) $this->times_billed;
}
/**
* Set the value of times_billed.
*
* @since 2.0.0
* @param int $times_billed Amount of times this membership got billed.
* @return void
*/
public function set_times_billed($times_billed): void {
$this->times_billed = $times_billed;
}
/**
* Increments times billed.
*
* @since 2.0.0
*
* @param integer $number Amount to increment by.
* @return \WP_Ultimo\Models\Membership
*/
public function add_to_times_billed($number = 1) {
$times_billed = absint($this->get_times_billed());
$this->set_times_billed(($times_billed + $number));
return $this;
}
/**
* Get the value of billing_cycles.
*
* @since 2.0.0
* @return int
*/
public function get_billing_cycles() {
return $this->billing_cycles;
}
/**
* Checks if this product recurs forever.
*
* @since 2.0.0
* @return boolean
*/
public function is_forever_recurring() {
return empty($this->get_billing_cycles());
}
/**
* Checks if we are on the max renewals.
*
* @since 2.0.0
* @return boolean
*/
public function at_maximum_renewals() {
if ( ! $this->is_forever_recurring()) {
return false;
}
$times_billed = $this->get_times_billed() - 1; // Subtract 1 to exclude initial payment.
$renew_times = $this->get_billing_cycles();
return $times_billed >= $renew_times;
}
/**
* Set the value of billing_cycles.
*
* @since 2.0.0
* @param int $billing_cycles Maximum times we should charge this membership.
* @return void
*/
public function set_billing_cycles($billing_cycles): void {
$this->billing_cycles = $billing_cycles;
}
/**
* Returns the default billing address.
*
* Classes that implement this trait need to implement
* this method.
*
* @since 2.0.0
* @return \WP_Ultimo\Objects\Billing_Address
*/
public function get_default_billing_address() {
$billing_address = new \WP_Ultimo\Objects\Billing_Address();
$customer = $this->get_customer();
if ($customer) {
return $customer->get_billing_address();
}
return $billing_address;
}
/**
* Checks if the current membership has a active status.
*
* @since 2.0.0
* @return boolean
*/
public function is_active() {
$active_statuses = [
Membership_Status::ACTIVE,
Membership_Status::ON_HOLD,
];
$active = in_array($this->status, $active_statuses, true);
return apply_filters('wu_membership_is_active', $active, $this);
}
/**
* Get the value of status.
*
* @since 2.0.0
* @return string
*/
public function get_status() {
return $this->status;
}
/**
* Set the value of status.
*
* @since 2.0.0
* @param string $status The membership status. Can be 'pending', 'active', 'on-hold', 'expired', 'cancelled' or other values added by third-party add-ons.
* @options \WP_Ultimo\Database\Payments\Payment_Status
* @return void
*/
public function set_status($status): void {
$this->status = $status;
}
/**
* Returns the Label for a given severity level.
*
* @since 2.0.0
* @return string
*/
public function get_status_label() {
$status = new Membership_Status($this->get_status());
return $status->get_label();
}
/**
* Gets the classes for a given class.
*
* @since 2.0.0
* @return string
*/
public function get_status_class() {
$status = new Membership_Status($this->get_status());
return $status->get_classes();
}
/**
* Get the value of gateway_customer_id.
*
* @since 2.0.0
* @return string
*/
public function get_gateway_customer_id() {
return $this->gateway_customer_id;
}
/**
* Set the value of gateway_customer_id.
*
* @since 2.0.0
* @param string $gateway_customer_id The ID of the customer on the payment gateway database.
* @return void
*/
public function set_gateway_customer_id($gateway_customer_id): void {
$this->gateway_customer_id = $gateway_customer_id;
}
/**
* Get the value of gateway_subscription_id.
*
* @since 2.0.0
* @return string
*/
public function get_gateway_subscription_id() {
return $this->gateway_subscription_id;
}
/**
* Set the value of gateway_subscription_id.
*
* @since 2.0.0
* @param string $gateway_subscription_id The ID of the subscription on the payment gateway database.
* @return void
*/
public function set_gateway_subscription_id($gateway_subscription_id): void {
$this->gateway_subscription_id = $gateway_subscription_id;
}
/**
* Get the value of gateway.
*
* @since 2.0.0
* @return string
*/
public function get_gateway() {
return $this->gateway;
}
/**
* Set the value of gateway.
*
* @since 2.0.0
* @param string $gateway ID of the gateway being used on this subscription.
* @return void
*/
public function set_gateway($gateway): void {
$this->gateway = $gateway;
}
/**
* Get the value of signup_method.
*
* @since 2.0.0
* @return string
*/
public function get_signup_method() {
return $this->signup_method;
}
/**
* Set the value of signup_method.
*
* @since 2.0.0
* @param string $signup_method Signup method used to create this membership.
* @return void
*/
public function set_signup_method($signup_method): void {
$this->signup_method = $signup_method;
}
/**
* Get the value of upgraded_from.
*
* @since 2.0.0
* @return string
*/
public function get_upgraded_from() {
return $this->upgraded_from;
}
/**
* Set the value of upgraded_from.
*
* @since 2.0.0
* @param int $upgraded_from Plan that this membership upgraded from.
* @return void
*/
public function set_upgraded_from($upgraded_from): void {
$this->upgraded_from = $upgraded_from;
}
/**
* Get the value of date_modified.
*
* @since 2.0.0
* @return string
*/
public function get_date_modified() {
return $this->date_modified;
}
/**
* Set the value of date_modified.
*
* @since 2.0.0
* @param string $date_modified Date this membership was last modified.
* @return void
*/
public function set_date_modified($date_modified): void {
$this->date_modified = $date_modified;
}
/**
* Get the value of disabled.
*
* @since 2.0.0
* @return bool
*/
public function is_disabled() {
return (bool) $this->disabled;
}
/**
* Set the value of disabled.
*
* @since 2.0.0
* @param bool $disabled If this membership is a disabled one.
* @return void
*/
public function set_disabled($disabled): void {
$this->disabled = (bool) $disabled;
}
/**
* Returns a list of payments associated with this membership.
*
* @since 2.0.0
* @param array $query Query arguments.
* @return array
*/
public function get_payments($query = []) {
$query = array_merge(
$query,
[
'membership_id' => $this->get_id(),
]
);
return wu_get_payments($query);
}
/**
* Returns the last pending payment for a membership.
*
* @since 2.0.0
* @return \WP_Ultimo\Models\Payment|false
*/
public function get_last_pending_payment() {
$payments = wu_get_payments(
[
'membership_id' => $this->get_id(),
'status' => 'pending',
'number' => 1,
'orderby' => 'id',
'order' => 'DESC',
'gateway_payment_id' => '',
]
);
return ! empty($payments) ? array_pop($payments) : false;
}
/**
* Returns the published sites attached to this membership.
*
* @since 2.0.18
* @return array
*/
public function get_published_sites() {
$sites = Site::query(
[
'meta_query' => [
'customer_id' => [
'key' => 'wu_membership_id',
'value' => $this->get_id(),
],
],
]
);
return $sites;
}
/**
* Returns the sites attached to this membership.
*
* @since 2.0.0
* @param bool $include_pending If we should include the pending site if exist.
* @return array
*/
public function get_sites($include_pending = true) {
$sites = Site::query(
[
'meta_query' => [
'customer_id' => [
'key' => 'wu_membership_id',
'value' => $this->get_id(),
],
],
]
);
$pending_site = $include_pending ? $this->get_pending_site() : false;
if ($pending_site) {
$pending_site->set_type('pending');
$sites[] = $pending_site;
}
return $sites;
}
/**
* Adds a pending site to the membership meta data.
*
* @since 2.0.0
*
* @param array $site_info Site info.
* @return bool
*/
public function create_pending_site($site_info): bool {
global $current_site;
$site_info = wp_parse_args(
$site_info,
[
'title' => '',
'domain' => $current_site->domain,
'path' => '',
'transient' => [],
'is_publishing' => false,
]
);
$site = new \WP_Ultimo\Models\Site($site_info);
return (bool) $this->update_meta('pending_site', $site);
}
/**
* Updates a pending site to the membership meta data.
*
* @since 2.0.11
*
* @param \WP_Ultimo\Models\Site $site Site info.
* @return bool
*/
public function update_pending_site($site) {
return $this->update_meta('pending_site', $site);
}
/**
* Returns the pending site, if any.
*
* @since 2.0.0
* @return \WP_Ultimo\Models\Site|false
*/
public function get_pending_site() {
return $this->get_meta('pending_site');
}
/**
* Published a pending site, but via job queue.
*
* @since 2.0.0
* @return void
*/
public function publish_pending_site_async(): void {
/*
* If the force sync setting is on, fallback to the sync version.
*/
if ((bool) wu_get_setting('force_publish_sites_sync', false)) {
$this->publish_pending_site();
return;
}
// We first try to generate the site through request to start earlier as possible.
$rest_path = add_query_arg(
[
'action' => 'wu_publish_pending_site',
'_ajax_nonce' => wp_create_nonce('wu_publish_pending_site'),
'membership_id' => $this->get_id(),
],
admin_url('admin-ajax.php')
);
if ( function_exists('fastcgi_finish_request') && version_compare(phpversion(), '7.0.16', '>=') ) {
// The server supports fastcgi, so we use this to guaranty that the function started before abort connection
wp_remote_request(
$rest_path,
[
'sslverify' => false,
]
);
} elseif (ignore_user_abort(true) !== ignore_user_abort(false)) {
// We do not have fastcgi but can make the request continue without listening
wp_remote_request(
$rest_path,
[
'sslverify' => false,
'blocking' => false,
]
);
}
wu_enqueue_async_action('wu_async_publish_pending_site', ['membership_id' => $this->get_id()], 'membership');
}
/**
* Publishes a pending site.
*
* @since 2.0.0
* @return true|\WP_Error
*/
public function publish_pending_site() {
/*
* Trigger event before the publication of a site.
*/
do_action('wu_before_pending_site_published', $this);
$pending_site = $this->get_pending_site();
if ( ! $pending_site) {
return true;
}
$is_publishing = $pending_site->is_publishing();
if ($is_publishing) {
return true;
}
$pending_site->set_publishing(true);
$this->update_pending_site($pending_site);
$pending_site->set_type('customer_owned');
$saved = $pending_site->save();
if (is_wp_error($saved)) {
if ($saved->get_error_code() === 'site_taken') {
/*
* If the site is already taken, we just delete the pending site.
* This is a workaround for cases where the publish is called twice
* even with is_publishing value as true.
*/
$this->delete_pending_site();
return true;
}
return $saved;
}
$this->delete_pending_site();
/*
* Trigger event that marks the publication of a site.
*/
do_action('wu_pending_site_published', $pending_site, $this);
return true;
}
/**
* Removes a pending site of a membership.
*
* @since 2.0.0
* @return bool
*/
public function delete_pending_site() {
return $this->delete_meta('pending_site');
}
/**
* Get is this product recurring?
*
* @since 2.0.0
* @return boolean
*/
public function is_recurring() {
return (bool) $this->recurring && (float) $this->get_amount() > 0;
}
/**
* Checks if this is a lifetime membership.
*
* @since 2.0.0
* @return boolean
*/
public function is_lifetime() {
return ! $this->is_recurring() && (empty($this->get_date_expiration()) || $this->get_date_expiration() === '0000-00-00 00:00:00');
}
/**
* Checks if this plan is free or not.
*
* @since 2.0.0
* @return boolean
*/
public function is_free() {
return $this->is_recurring() === false && empty($this->get_initial_amount());
}
/**
* Set is this product recurring?
*
* @since 2.0.0
* @param boolean $recurring If this membership is recurring (true), which means the customer paid a defined amount each period of time, or not recurring (false).
* @return void
*/
public function set_recurring($recurring): void {
$this->recurring = (bool) $recurring;
}
/**
* Gets the total grossed by the membership so far.
*
* @since 2.0.0
* @return float
*/
public function get_total_grossed() {
global $wpdb;
static $sum;
if (null === $sum) {
$sum = $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(total) FROM {$wpdb->base_prefix}wu_payments WHERE parent_id = 0 AND membership_id = %d",
$this->get_id()
)
);
}
return $sum;
}
/**
* By default, we just use the to_array method, but you can rewrite this.
*
* @since 2.0.0
* @return array
*/
public function to_search_results() {
$search_result = $this->to_array();
$search_result['customer'] = null;
$search_result['display_name'] = '';
if ($this->get_customer()) {
$search_result['customer'] = $this->get_customer()->to_search_results();
$search_result['display_name'] = $search_result['customer']['display_name'];
}
$search_result['formatted_price'] = $this->get_price_description();
$search_result['reference_code'] = $this->get_hash();
return $search_result;
}
/**
* Renews the membership by updating status and expiration date.
*
* Does NOT handle payment processing for the renewal. This should be called after receiving a renewal payment.
*
* @since 2.0.0
*
* @param bool $auto_renew Whether or not the membership is recurring.
* @param string $status Membership status.
* @param string $expiration Membership expiration date in MySQL format.
* @return bool Whether or not the renewal was successful.
*/
public function renew($auto_renew = false, $status = 'active', $expiration = ''): bool {
$id = $this->get_id();
$plan_id = $this->get_plan_id();
wu_log_add("membership-{$id}", sprintf('Starting membership renewal for membership #%d. Membership Level ID: %d; Current Expiration Date: %s', $id, $plan_id, $this->get_date_expiration()));
if (empty($plan_id)) {
return false;
}
// Bail if this has a payment plan and it's completed - prevents renewals from running after the fact.
if ( ! $this->is_forever_recurring() && $this->at_maximum_renewals()) {
return false;
}
$plan = wu_get_product($plan_id);
if ( ! $expiration) {
$expiration = $this->calculate_expiration(! $this->is_recurring());
/**
* Filters the calculated expiration date to be set after the renewal.
*
* @param string $expiration Calculated expiration date.
* @param Product $plan Membership level object.
* @param int $membership_id The ID of the membership.
* @param Membership $this Membership object.
*
* @since 2.0.0
*/
$expiration = apply_filters('wu_membership_renewal_expiration_date', $expiration, $plan, $this->get_id(), $this);
}
/**
* Triggers before the membership renewal.
*
* @param string $expiration New expiration date to be set.
* @param int $membership_id The ID of the membership.
* @param Membership $this Membership object.
*
* @since 2.0
*/
do_action('wu_membership_pre_renew', $expiration, $this->get_id(), $this);
$this->set_date_expiration($expiration);
if ( ! empty($status)) {
$this->set_status($status);
}
$this->set_auto_renew($auto_renew);
$this->set_date_renewed(wu_get_current_time('mysql')); // Current time.
if ($this->get_user_id()) {
delete_user_meta($this->get_user_id(), 'wu_expired_email_sent');
}
$status = $this->save();
if (is_wp_error($status)) {
return $status;
}
/**
* Triggers after the membership renewal.
*
* @param string $expiration New expiration date to be set.
* @param int $membership_id The ID of the membership.
* @param Membership $this Membership object.
*
* @since 2.0
*/
do_action('wu_membership_post_renew', $expiration, $this->get_id(), $this);
wu_log_add("membership-{$id}", sprintf('Completed membership renewal for membership #%d. Membership Level ID: %d; New Expiration Date: %s; New Status: %s', $id, $plan_id, $expiration, $this->get_status()));
return true;
}
/**
* Changes the membership status to "cancelled".
*
* Does NOT handle actual cancellation of subscription payments, that is done in rcp_process_member_cancellation().
* This should be called after a member is successfully cancelled.
*
* @since 2.0.0
* @param string $reason Reason for cancellation.
* @return void
*/
public function cancel($reason = ''): void {
if ($this->get_status() === Membership_Status::CANCELLED) {
return; // Already cancelled
}
/**
* Triggers before the membership is cancelled.
*
* @param int $membership_id The ID of the membership.
* @param \WP_Ultimo\Models\Membership $this Membership object.
*
* @since 2.0
*/
do_action('wu_membership_pre_cancel', $this->get_id(), $this);
// Change status.
$this->set_status(Membership_Status::CANCELLED);
$this->set_date_cancellation(wu_get_current_time('mysql'));
if ( ! empty($reason)) {
$this->set_cancellation_reason($reason);
}
$this->save();
/**
* Triggers after the membership is cancelled.
*
* This triggers the cancellation email.
*
* @param int $membership_id The ID of the membership.
* @param \WP_Ultimo\Models\Membership $this Membership object.
*
* @since 2.0
*/
do_action('wu_membership_post_cancel', $this->get_id(), $this);
}
/**
* Returns the number of days still left in the cycle.
*
* @since 2.0.0
* @return int
*/
public function get_remaining_days_in_cycle() {
/*
* If this is a lifetime membership, we have unlimited days. Large number.
*/
if ( ! $this->is_recurring()) {
return 10000;
}
/*
* We need to have a valid expiration date.
*/
if (empty($this->get_date_expiration()) || ! wu_validate_date($this->get_date_expiration())) {
return 0;
}
/*
* Otherwise, we need to check based on the
* expiration date.
*/
$expiration_date = wu_date($this->get_date_expiration());
$today = wu_date();
/*
* If today is larger than the expiration date,
* it means that the customer used all the membership time.
* There's nothing to pro-rate in that case.
*/
if ($today > $expiration_date) {
return 0;
}
return floor($today->diff($expiration_date)->days);
}
/**
* List of limitations that need to be merged.
*
* Every model that is limitable (imports this trait)
* needs to declare explicitly the limitations that need to be
* merged. This allows us to chain the merges, and gives us
* a final list of limitations at the end of the process.
*
* In the case of membership, we need to mash up
* all the limitations associated with the membership
* plan and additional packages.
*
* @see \WP_Ultimo\Models\Traits\Trait_Limitable
* @since 2.0.0
* @return array
*/
public function limitations_to_merge() {
$limitations_to_merge = [];
$product_ids = [$this->get_plan_id()];
$product_ids = array_merge($this->get_addon_ids(), $product_ids);
foreach ($product_ids as $product_id) {
$amount = 1;
if (is_array($this->addon_products) && isset($this->addon_products[ $product_id ])) {
$amount = $this->addon_products[ $product_id ];
}
$limitation = \WP_Ultimo\Objects\Limitations::early_get_limitations('product', $product_id);
for ($i = 0; $i < $amount; $i++) {
$limitations_to_merge[] = $limitation;
}
}
return $limitations_to_merge;
}
/**
* Checks if the membership has product changes.
*
* @since 2.0.10
* @return boolean
*/
protected function has_product_changes() {
if (empty($this->_compiled_product_list)) {
return false;
}
$old_products = $this->_compiled_product_list;
$new_products = $this->get_all_products();
if (count($old_products) !== count($new_products)) {
return true;
}
$compiled_old = array_map(fn($product) => $product['product']->get_id() . '|' . $product['quantity'], $old_products);
$compiled_new = array_map(fn($product) => $product['product']->get_id() . '|' . $product['quantity'], $new_products);
return array_diff($compiled_old, $compiled_new) || array_diff($compiled_new, $compiled_old);
}
/**
* Checks if the membership has gateway changes.
*
* @since 2.0.15
* @return boolean
*/
protected function has_gateway_changes() {
$has_change = false;
$current_gateway = [
'gateway' => $this->get_gateway(),
'gateway_customer_id' => $this->get_gateway_customer_id(),
'gateway_subscription_id' => $this->get_gateway_subscription_id(),
];
foreach ($this->_gateway_info as $key => $value) {
if ($current_gateway[ $key ] !== $value) {
$has_change = true;
break;
}
}
return $has_change;
}
/**
* Get the number of remaining sites available to this membership.
*
* This means sites that can be still added.
*
* @since 2.0.11
* @return int
*/
public function get_remaining_sites() {
$limit = $this->get_limitations()->sites->get_limit();
if ( ! $this->get_limitations()->sites->is_enabled()) {
return PHP_INT_MAX;
}
$limit = '' === $limit ? 1 : $limit;
return $limit - count($this->get_sites());
}
/**
* Checks if the current membership has remaining sites available.
*
* @since 2.0.11
* @return boolean
*/
public function has_remaining_sites() {
return $this->get_remaining_sites() >= 1;
}
/**
* Checks if the current membership is current trialing.
*
* @since 2.0.18
* @return boolean
*/
public function is_trialing() {
return $this->get_date_trial_end() > gmdate('Y-m-d 23:59:59');
}
/**
* Save (create or update) the model on the database.
*
* @since 2.0.0
*/
public function save() {
// Set trial status if needed
if ($this->get_status() === Membership_Status::PENDING && $this->is_trialing() && ! $this->get_last_pending_payment()) {
if ( ! wu_get_setting('enable_email_verification', true) || $this->get_customer()->get_email_verification() !== 'pending') {
$this->set_status(Membership_Status::TRIALING);
}
}
if ($this->has_product_changes()) {
wu_enqueue_async_action(
'wu_async_after_membership_update_products',
[
'membership_id' => $this->get_id(),
],
'membership'
);
}
// If membership is cancelled, we need to cancel the subscription on the gateway.
if ($this->get_status() === Membership_Status::CANCELLED) {
/**
* Here we just need to change the used gateway,
* this will trigger the cancellation process if needed.
*/
$this->set_gateway('');
$this->set_gateway_customer_id('');
$this->set_gateway_subscription_id('');
$this->set_auto_renew(false);
}
if ($this->has_gateway_changes() && $this->_gateway_info['gateway']) {
/**
* Lets deal with gateway change processing
* the cancelation of the original one
*/
$gateway = wu_get_gateway($this->_gateway_info['gateway']);
if ($gateway) {
$membership_old = clone $this;
$membership_old->set_gateway($this->_gateway_info['gateway']);
$membership_old->set_gateway_customer_id($this->_gateway_info['gateway_customer_id']);
$membership_old->set_gateway_subscription_id($this->_gateway_info['gateway_subscription_id']);
$gateway->process_cancellation($membership_old, $this->get_customer());
}
}
// Run it for existing memberships without gateway changes only.
if ($this->get_id() && ! $this->has_gateway_changes()) {
$original = $this->_get_original();
$has_amount_change = (float) $this->get_amount() !== (float) wu_get_isset($original, 'amount');
$has_duration_change = $this->get_duration() !== absint(wu_get_isset($original, 'duration')) || $this->get_duration_unit() !== wu_get_isset($original, 'duration_unit');
if ($this->has_product_changes() || $has_amount_change || $has_duration_change) {
/**
* If the membership has product changes, amount changes or duration changes
* we need to reflect those changes on the gateway.
*/
$gateway = wu_get_gateway($this->get_gateway());
if ($gateway) {
$gateway_update = $gateway->process_membership_update($this, $this->get_customer());
if (is_wp_error($gateway_update)) {
return $gateway_update;
}
}
}
}
return parent::save();
}
/**
* Delete the model from the database.
*
* Here we also need to cancel the subscription on the gateway.
*
* @since 2.1.2
* @return \WP_Error|bool
*/
public function delete() {
$gateway = wu_get_gateway($this->get_gateway());
if ($gateway) {
$gateway->process_cancellation($this, $this->get_customer());
}
return parent::delete();
}
}