Files
woocommerce-subscriptions/includes/class-wcs-retry-manager.php
Prospress Inc ed27f81d70 2.2.10
2017-07-21 16:16:16 +02:00

391 lines
14 KiB
PHP

<?php
/**
* Manage the process of retrying a failed renewal payment that previously failed.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Retry_Manager
* @category Class
* @author Prospress
* @since 2.1
*/
require_once( 'payment-retry/class-wcs-retry-admin.php' );
class WCS_Retry_Manager {
/* the rules that control the retry schedule and behaviour of each retry */
protected static $retry_rules = array();
/* an instance of the class responsible for storing retry data */
protected static $store;
/* the setting ID for enabling/disabling the automatic retry system */
protected static $setting_id;
/* property to store the instance of WCS_Retry_Admin */
protected static $admin;
/**
* Attach callbacks and set the retry rules
*
* @codeCoverageIgnore
* @since 2.1
*/
public static function init() {
self::$setting_id = WC_Subscriptions_Admin::$option_prefix . '_enable_retry';
self::$admin = new WCS_Retry_Admin( self::$setting_id );
if ( self::is_retry_enabled() ) {
self::load_classes();
add_filter( 'init', array( self::store(), 'init' ) );
add_filter( 'woocommerce_valid_order_statuses_for_payment', __CLASS__ . '::check_order_statuses_for_payment', 10, 2 );
add_filter( 'woocommerce_subscription_dates', __CLASS__ . '::add_retry_date_type' );
add_action( 'delete_post', __CLASS__ . '::maybe_cancel_retry_for_order' );
add_action( 'wp_trash_post', __CLASS__ . '::maybe_cancel_retry_for_order' );
add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::maybe_cancel_retry', 0, 3 );
add_action( 'woocommerce_subscriptions_retry_status_updated', __CLASS__ . '::maybe_delete_payment_retry_date', 0, 2 );
add_action( 'woocommerce_subscription_renewal_payment_failed', __CLASS__ . '::maybe_apply_retry_rule', 10, 2 );
add_action( 'woocommerce_scheduled_subscription_payment_retry', __CLASS__ . '::maybe_retry_payment' );
add_filter( 'woocommerce_subscriptions_is_failed_renewal_order', __CLASS__ . '::compare_order_and_retry_statuses', 10, 3 );
}
}
/**
* Adds any extra status that may be needed for a given order to check if it may
* need payment
*
* @param Array $statuses
* @param WC_Order $order
* @return array
* @since 2.2.1
*/
public static function check_order_statuses_for_payment( $statuses, $order ) {
$last_retry = self::store()->get_last_retry_for_order( $order );
if ( $last_retry ) {
$statuses[] = $last_retry->get_rule()->get_status_to_apply( 'order' );
$statuses = array_unique( $statuses );
}
return $statuses;
}
/**
* A helper function to check if the retry system has been enabled or not
*
* @since 2.1
*/
public static function is_retry_enabled() {
return apply_filters( 'wcs_is_retry_enabled', ( 'yes' == get_option( self::$setting_id, 'no' ) ) ? true : false );
}
/**
* Load all the retry classes if the retry system is enabled
*
* @codeCoverageIgnore
* @since 2.1
*/
protected static function load_classes() {
require_once( 'abstracts/abstract-wcs-retry-store.php' );
require_once( 'payment-retry/class-wcs-retry.php' );
require_once( 'payment-retry/class-wcs-retry-rule.php' );
require_once( 'payment-retry/class-wcs-retry-rules.php' );
require_once( 'payment-retry/class-wcs-retry-post-store.php' );
require_once( 'payment-retry/class-wcs-retry-email.php' );
require_once( 'admin/meta-boxes/class-wcs-meta-box-payment-retries.php' );
}
/**
* Add a renewal retry date type to Subscriptions date types
*
* @since 2.1
*/
public static function add_retry_date_type( $subscription_date_types ) {
$subscription_date_types = wcs_array_insert_after( 'next_payment', $subscription_date_types, 'payment_retry', _x( 'Renewal Payment Retry', 'table heading', 'woocommerce-subscriptions' ) );
return $subscription_date_types;
}
/**
* When a subscription's status is updated, if the new status isn't the expected retry subscription status, cancel the retry.
*
* @param object $subscription An instance of a WC_Subscription object
* @param string $new_status A valid subscription status
* @param string $old_status A valid subscription status
*/
public static function maybe_cancel_retry( $subscription, $new_status, $old_status ) {
if ( $subscription->get_date( 'payment_retry' ) > 0 ) {
$last_order = $subscription->get_last_order( 'all' );
$last_retry = ( $last_order ) ? self::store()->get_last_retry_for_order( wcs_get_objects_property( $last_order, 'id' ) ) : null;
if ( null !== $last_retry && 'cancelled' !== $last_retry->get_status() && null !== ( $last_retry_rule = $last_retry->get_rule() ) ) {
$retry_subscription_status = $last_retry_rule->get_status_to_apply( 'subscription' );
$applying_retry_rule = did_action( 'woocommerce_subscriptions_before_apply_retry_rule' ) !== did_action( 'woocommerce_subscriptions_after_apply_retry_rule' );
$retrying_payment = did_action( 'woocommerce_subscriptions_before_payment_retry' ) !== did_action( 'woocommerce_subscriptions_after_payment_retry' );
// If the new status isn't the expected retry subscription status and we aren't in the process of applying a retry rule or retrying payment, cancel the retry
if ( $new_status != $retry_subscription_status && ! $applying_retry_rule && ! $retrying_payment ) {
$last_retry->update_status( 'cancelled' );
$subscription->delete_date( 'payment_retry' );
}
}
}
}
/**
* When a (renewal) order is trashed or deleted, make sure its retries are also trashed/deleted.
*
* @param int $post_id
*/
public static function maybe_cancel_retry_for_order( $post_id ) {
if ( 'shop_order' == get_post_type( $post_id ) ) {
$last_retry = self::store()->get_last_retry_for_order( $post_id );
// Make sure the last retry is cancelled first so that it is unscheduled via self::maybe_delete_payment_retry_date()
if ( null !== $last_retry && 'cancelled' !== $last_retry->get_status() ) {
$last_retry->update_status( 'cancelled' );
}
foreach ( self::store()->get_retry_ids_for_order( $post_id ) as $retry_id ) {
wp_trash_post( $retry_id );
}
}
}
/**
* When a retry's status is updated, if it's no longer pending or processing and it's the most recent retry,
* delete the retry date on the subscriptions related to the order
*
* @param object $retry An instance of a WCS_Retry object
* @param string $new_status A valid retry status
*/
public static function maybe_delete_payment_retry_date( $retry, $new_status ) {
if ( ! in_array( $new_status, array( 'pending', 'processing' ) ) ) {
$last_retry = self::store()->get_last_retry_for_order( $retry->get_order_id() );
if ( $retry->get_id() === $last_retry->get_id() ) {
foreach ( wcs_get_subscriptions_for_renewal_order( $retry->get_order_id() ) as $subscription ) {
$subscription->delete_date( 'payment_retry' );
}
}
}
}
/**
* When a payment fails, apply a retry rule, if one exists that applies to this failure.
*
* @param WC_Subscription The subscription on which the payment failed
* @param WC_Order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param)
* @since 2.1
*/
public static function maybe_apply_retry_rule( $subscription, $last_order ) {
if ( $subscription->is_manual() || ! $subscription->payment_method_supports( 'subscription_date_changes' ) ) {
return;
}
$retry_count = self::store()->get_retry_count_for_order( wcs_get_objects_property( $last_order, 'id' ) );
if ( self::rules()->has_rule( $retry_count, wcs_get_objects_property( $last_order, 'id' ) ) ) {
$retry_rule = self::rules()->get_rule( $retry_count, wcs_get_objects_property( $last_order, 'id' ) );
do_action( 'woocommerce_subscriptions_before_apply_retry_rule', $retry_rule, $last_order, $subscription );
$retry_id = self::store()->save( new WCS_Retry( array(
'status' => 'pending',
'order_id' => wcs_get_objects_property( $last_order, 'id' ),
'date_gmt' => gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) + $retry_rule->get_retry_interval() ),
'rule_raw' => $retry_rule->get_raw_data(),
) ) );
foreach ( array( 'order' => $last_order, 'subscription' => $subscription ) as $object_key => $object ) {
$new_status = $retry_rule->get_status_to_apply( $object_key );
if ( '' !== $new_status && ! $object->has_status( $new_status ) ) {
$object->update_status( $new_status, _x( 'Retry rule applied:', 'used in order note as reason for why status changed', 'woocommerce-subscriptions' ) );
}
}
if ( $retry_rule->get_retry_interval() > 0 ) {
// by calling this after changing the status, this will also schedule the 'woocommerce_scheduled_subscription_payment_retry' action
$subscription->update_dates( array( 'payment_retry' => gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) + $retry_rule->get_retry_interval( $retry_count ) ) ) );
}
do_action( 'woocommerce_subscriptions_after_apply_retry_rule', $retry_rule, $last_order, $subscription );
}
}
/**
* When a retry hook is triggered, check if the rules for that retry are still valid
* and if so, retry the payment.
*
* @param WC_Order|int The order on which the payment failed
* @since 2.1
*/
public static function maybe_retry_payment( $last_order ) {
if ( ! is_object( $last_order ) ) {
$last_order = wc_get_order( $last_order );
}
if ( false === $last_order ) {
return;
}
$subscriptions = wcs_get_subscriptions_for_renewal_order( $last_order );
$last_retry = self::store()->get_last_retry_for_order( wcs_get_objects_property( $last_order, 'id' ) );
// we only need to retry the payment if we have applied a retry rule for the order and it still needs payment
if ( null !== $last_retry && 'pending' === $last_retry->get_status() ) {
do_action( 'woocommerce_subscriptions_before_payment_retry', $last_retry, $last_order );
if ( $last_order->needs_payment() ) {
$last_retry->update_status( 'processing' );
$expected_order_status = $last_retry->get_rule()->get_status_to_apply( 'order' );
$valid_order_status = ( '' == $expected_order_status || $last_order->has_status( $expected_order_status ) ) ? true : false;
$expected_subscription_status = $last_retry->get_rule()->get_status_to_apply( 'subscription' );
if ( '' == $expected_subscription_status ) {
$valid_subscription_status = true;
} else {
$valid_subscription_status = true;
foreach ( $subscriptions as $subscription ) {
if ( ! $subscription->has_status( $expected_subscription_status ) ) {
$valid_subscription_status = false;
break;
}
}
}
// if both statuses are still the same or there no special status was applied and the order still needs payment (i.e. there has been no manual intervention), trigger the payment hook
if ( $valid_order_status && $valid_subscription_status ) {
$last_order->update_status( 'pending', _x( 'Subscription renewal payment retry:', 'used in order note as reason for why order status changed', 'woocommerce-subscriptions' ), true );
// Make sure the subscription is on hold in case something goes wrong while trying to process renewal and in case gateways expect the subscription to be on-hold, which is normally the case with a renewal payment
foreach ( $subscriptions as $subscription ) {
$subscription->update_status( 'on-hold', _x( 'Subscription renewal payment retry:', 'used in order note as reason for why subscription status changed', 'woocommerce-subscriptions' ) );
}
WC_Subscriptions_Payment_Gateways::trigger_gateway_renewal_payment_hook( $last_order );
// Now that we've attempted to process the payment, refresh the order
$last_order = wc_get_order( wcs_get_objects_property( $last_order, 'id' ) );
// if the order still needs payment, payment failed
if ( $last_order->needs_payment() ) {
$last_retry->update_status( 'failed' );
} else {
$last_retry->update_status( 'complete' );
}
} else {
// order or subscription statuses have been manually updated, so we'll cancel the retry
$last_retry->update_status( 'cancelled' );
}
} else {
// last order must have been paid for some other way, so we'll cancel the retry
$last_retry->update_status( 'cancelled' );
}
do_action( 'woocommerce_subscriptions_after_payment_retry', $last_retry, $last_order );
}
}
/**
* Determines if a renewal order and the last retry statuses are the same (used to determine if a payment method
* change is needed)
*
* @since 2.2.8
*/
public static function compare_order_and_retry_statuses( $is_failed_order, $order_id, $order_status ) {
$last_retry = self::store()->get_last_retry_for_order( $order_id );
if ( null !== $last_retry && $order_status === $last_retry->get_rule()->get_status_to_apply( 'order' ) ) {
$is_failed_order = true;
}
return $is_failed_order;
}
/**
* Access the object used to interface with the database
*
* @since 2.1
*/
public static function store() {
if ( empty( self::$store ) ) {
$class = self::get_store_class();
self::$store = new $class();
}
return self::$store;
}
/**
* Get the class used for instantiating retry storage via self::store()
*
* @since 2.1
*/
protected static function get_store_class() {
return apply_filters( 'wcs_retry_store_class', 'WCS_Retry_Post_Store' );
}
/**
* Setup and access the object used to interface with retry rules
*
* @since 2.1
*/
public static function rules() {
if ( empty( self::$retry_rules ) ) {
$class = self::get_rules_class();
self::$retry_rules = new $class();
}
return self::$retry_rules;
}
/**
* Get the class used for instantiating retry rules via self::rules()
*
* @since 2.1
*/
protected static function get_rules_class() {
return apply_filters( 'wcs_retry_rules_class', 'WCS_Retry_Rules' );
}
}
WCS_Retry_Manager::init();