PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-content/plugins/woocommerce-subscriptions-master/includes/class-wcs-retry-manager.php

https://bitbucket.org/tristangemus/tribe-demo
PHP | 390 lines | 187 code | 90 blank | 113 comment | 44 complexity | cf59cd145151d5ac5b7ac5b873b17dc5 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. /**
  3. * Manage the process of retrying a failed renewal payment that previously failed.
  4. *
  5. * @package WooCommerce Subscriptions
  6. * @subpackage WCS_Retry_Manager
  7. * @category Class
  8. * @author Prospress
  9. * @since 2.1
  10. */
  11. require_once( 'payment-retry/class-wcs-retry-admin.php' );
  12. class WCS_Retry_Manager {
  13. /* the rules that control the retry schedule and behaviour of each retry */
  14. protected static $retry_rules = array();
  15. /* an instance of the class responsible for storing retry data */
  16. protected static $store;
  17. /* the setting ID for enabling/disabling the automatic retry system */
  18. protected static $setting_id;
  19. /* property to store the instance of WCS_Retry_Admin */
  20. protected static $admin;
  21. /**
  22. * Attach callbacks and set the retry rules
  23. *
  24. * @codeCoverageIgnore
  25. * @since 2.1
  26. */
  27. public static function init() {
  28. self::$setting_id = WC_Subscriptions_Admin::$option_prefix . '_enable_retry';
  29. self::$admin = new WCS_Retry_Admin( self::$setting_id );
  30. if ( self::is_retry_enabled() ) {
  31. self::load_classes();
  32. add_filter( 'init', array( self::store(), 'init' ) );
  33. add_filter( 'woocommerce_valid_order_statuses_for_payment', __CLASS__ . '::check_order_statuses_for_payment', 10, 2 );
  34. add_filter( 'woocommerce_subscription_dates', __CLASS__ . '::add_retry_date_type' );
  35. add_action( 'delete_post', __CLASS__ . '::maybe_cancel_retry_for_order' );
  36. add_action( 'wp_trash_post', __CLASS__ . '::maybe_cancel_retry_for_order' );
  37. add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::maybe_cancel_retry', 0, 3 );
  38. add_action( 'woocommerce_subscriptions_retry_status_updated', __CLASS__ . '::maybe_delete_payment_retry_date', 0, 2 );
  39. add_action( 'woocommerce_subscription_renewal_payment_failed', __CLASS__ . '::maybe_apply_retry_rule', 10, 2 );
  40. add_action( 'woocommerce_scheduled_subscription_payment_retry', __CLASS__ . '::maybe_retry_payment' );
  41. add_filter( 'woocommerce_subscriptions_is_failed_renewal_order', __CLASS__ . '::compare_order_and_retry_statuses', 10, 3 );
  42. }
  43. }
  44. /**
  45. * Adds any extra status that may be needed for a given order to check if it may
  46. * need payment
  47. *
  48. * @param Array $statuses
  49. * @param WC_Order $order
  50. * @return array
  51. * @since 2.2.1
  52. */
  53. public static function check_order_statuses_for_payment( $statuses, $order ) {
  54. $last_retry = self::store()->get_last_retry_for_order( $order );
  55. if ( $last_retry ) {
  56. $statuses[] = $last_retry->get_rule()->get_status_to_apply( 'order' );
  57. $statuses = array_unique( $statuses );
  58. }
  59. return $statuses;
  60. }
  61. /**
  62. * A helper function to check if the retry system has been enabled or not
  63. *
  64. * @since 2.1
  65. */
  66. public static function is_retry_enabled() {
  67. return apply_filters( 'wcs_is_retry_enabled', ( 'yes' == get_option( self::$setting_id, 'no' ) ) ? true : false );
  68. }
  69. /**
  70. * Load all the retry classes if the retry system is enabled
  71. *
  72. * @codeCoverageIgnore
  73. * @since 2.1
  74. */
  75. protected static function load_classes() {
  76. require_once( 'abstracts/abstract-wcs-retry-store.php' );
  77. require_once( 'payment-retry/class-wcs-retry.php' );
  78. require_once( 'payment-retry/class-wcs-retry-rule.php' );
  79. require_once( 'payment-retry/class-wcs-retry-rules.php' );
  80. require_once( 'payment-retry/class-wcs-retry-post-store.php' );
  81. require_once( 'payment-retry/class-wcs-retry-email.php' );
  82. require_once( 'admin/meta-boxes/class-wcs-meta-box-payment-retries.php' );
  83. }
  84. /**
  85. * Add a renewal retry date type to Subscriptions date types
  86. *
  87. * @since 2.1
  88. */
  89. public static function add_retry_date_type( $subscription_date_types ) {
  90. $subscription_date_types = wcs_array_insert_after( 'next_payment', $subscription_date_types, 'payment_retry', _x( 'Renewal Payment Retry', 'table heading', 'woocommerce-subscriptions' ) );
  91. return $subscription_date_types;
  92. }
  93. /**
  94. * When a subscription's status is updated, if the new status isn't the expected retry subscription status, cancel the retry.
  95. *
  96. * @param object $subscription An instance of a WC_Subscription object
  97. * @param string $new_status A valid subscription status
  98. * @param string $old_status A valid subscription status
  99. */
  100. public static function maybe_cancel_retry( $subscription, $new_status, $old_status ) {
  101. if ( $subscription->get_date( 'payment_retry' ) > 0 ) {
  102. $last_order = $subscription->get_last_order( 'all' );
  103. $last_retry = ( $last_order ) ? self::store()->get_last_retry_for_order( wcs_get_objects_property( $last_order, 'id' ) ) : null;
  104. if ( null !== $last_retry && 'cancelled' !== $last_retry->get_status() && null !== ( $last_retry_rule = $last_retry->get_rule() ) ) {
  105. $retry_subscription_status = $last_retry_rule->get_status_to_apply( 'subscription' );
  106. $applying_retry_rule = did_action( 'woocommerce_subscriptions_before_apply_retry_rule' ) !== did_action( 'woocommerce_subscriptions_after_apply_retry_rule' );
  107. $retrying_payment = did_action( 'woocommerce_subscriptions_before_payment_retry' ) !== did_action( 'woocommerce_subscriptions_after_payment_retry' );
  108. // 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
  109. if ( $new_status != $retry_subscription_status && ! $applying_retry_rule && ! $retrying_payment ) {
  110. $last_retry->update_status( 'cancelled' );
  111. $subscription->delete_date( 'payment_retry' );
  112. }
  113. }
  114. }
  115. }
  116. /**
  117. * When a (renewal) order is trashed or deleted, make sure its retries are also trashed/deleted.
  118. *
  119. * @param int $post_id
  120. */
  121. public static function maybe_cancel_retry_for_order( $post_id ) {
  122. if ( 'shop_order' == get_post_type( $post_id ) ) {
  123. $last_retry = self::store()->get_last_retry_for_order( $post_id );
  124. // Make sure the last retry is cancelled first so that it is unscheduled via self::maybe_delete_payment_retry_date()
  125. if ( null !== $last_retry && 'cancelled' !== $last_retry->get_status() ) {
  126. $last_retry->update_status( 'cancelled' );
  127. }
  128. foreach ( self::store()->get_retry_ids_for_order( $post_id ) as $retry_id ) {
  129. wp_trash_post( $retry_id );
  130. }
  131. }
  132. }
  133. /**
  134. * When a retry's status is updated, if it's no longer pending or processing and it's the most recent retry,
  135. * delete the retry date on the subscriptions related to the order
  136. *
  137. * @param object $retry An instance of a WCS_Retry object
  138. * @param string $new_status A valid retry status
  139. */
  140. public static function maybe_delete_payment_retry_date( $retry, $new_status ) {
  141. if ( ! in_array( $new_status, array( 'pending', 'processing' ) ) ) {
  142. $last_retry = self::store()->get_last_retry_for_order( $retry->get_order_id() );
  143. if ( $retry->get_id() === $last_retry->get_id() ) {
  144. foreach ( wcs_get_subscriptions_for_renewal_order( $retry->get_order_id() ) as $subscription ) {
  145. $subscription->delete_date( 'payment_retry' );
  146. }
  147. }
  148. }
  149. }
  150. /**
  151. * When a payment fails, apply a retry rule, if one exists that applies to this failure.
  152. *
  153. * @param WC_Subscription The subscription on which the payment failed
  154. * @param WC_Order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param)
  155. * @since 2.1
  156. */
  157. public static function maybe_apply_retry_rule( $subscription, $last_order ) {
  158. if ( $subscription->is_manual() || ! $subscription->payment_method_supports( 'subscription_date_changes' ) ) {
  159. return;
  160. }
  161. $retry_count = self::store()->get_retry_count_for_order( wcs_get_objects_property( $last_order, 'id' ) );
  162. if ( self::rules()->has_rule( $retry_count, wcs_get_objects_property( $last_order, 'id' ) ) ) {
  163. $retry_rule = self::rules()->get_rule( $retry_count, wcs_get_objects_property( $last_order, 'id' ) );
  164. do_action( 'woocommerce_subscriptions_before_apply_retry_rule', $retry_rule, $last_order, $subscription );
  165. $retry_id = self::store()->save( new WCS_Retry( array(
  166. 'status' => 'pending',
  167. 'order_id' => wcs_get_objects_property( $last_order, 'id' ),
  168. 'date_gmt' => gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) + $retry_rule->get_retry_interval() ),
  169. 'rule_raw' => $retry_rule->get_raw_data(),
  170. ) ) );
  171. foreach ( array( 'order' => $last_order, 'subscription' => $subscription ) as $object_key => $object ) {
  172. $new_status = $retry_rule->get_status_to_apply( $object_key );
  173. if ( '' !== $new_status && ! $object->has_status( $new_status ) ) {
  174. $object->update_status( $new_status, _x( 'Retry rule applied:', 'used in order note as reason for why status changed', 'woocommerce-subscriptions' ) );
  175. }
  176. }
  177. if ( $retry_rule->get_retry_interval() > 0 ) {
  178. // by calling this after changing the status, this will also schedule the 'woocommerce_scheduled_subscription_payment_retry' action
  179. $subscription->update_dates( array( 'payment_retry' => gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) + $retry_rule->get_retry_interval( $retry_count ) ) ) );
  180. }
  181. do_action( 'woocommerce_subscriptions_after_apply_retry_rule', $retry_rule, $last_order, $subscription );
  182. }
  183. }
  184. /**
  185. * When a retry hook is triggered, check if the rules for that retry are still valid
  186. * and if so, retry the payment.
  187. *
  188. * @param WC_Order|int The order on which the payment failed
  189. * @since 2.1
  190. */
  191. public static function maybe_retry_payment( $last_order ) {
  192. if ( ! is_object( $last_order ) ) {
  193. $last_order = wc_get_order( $last_order );
  194. }
  195. if ( false === $last_order ) {
  196. return;
  197. }
  198. $subscriptions = wcs_get_subscriptions_for_renewal_order( $last_order );
  199. $last_retry = self::store()->get_last_retry_for_order( wcs_get_objects_property( $last_order, 'id' ) );
  200. // we only need to retry the payment if we have applied a retry rule for the order and it still needs payment
  201. if ( null !== $last_retry && 'pending' === $last_retry->get_status() ) {
  202. do_action( 'woocommerce_subscriptions_before_payment_retry', $last_retry, $last_order );
  203. if ( $last_order->needs_payment() ) {
  204. $last_retry->update_status( 'processing' );
  205. $expected_order_status = $last_retry->get_rule()->get_status_to_apply( 'order' );
  206. $valid_order_status = ( '' == $expected_order_status || $last_order->has_status( $expected_order_status ) ) ? true : false;
  207. $expected_subscription_status = $last_retry->get_rule()->get_status_to_apply( 'subscription' );
  208. if ( '' == $expected_subscription_status ) {
  209. $valid_subscription_status = true;
  210. } else {
  211. $valid_subscription_status = true;
  212. foreach ( $subscriptions as $subscription ) {
  213. if ( ! $subscription->has_status( $expected_subscription_status ) ) {
  214. $valid_subscription_status = false;
  215. break;
  216. }
  217. }
  218. }
  219. // 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
  220. if ( $valid_order_status && $valid_subscription_status ) {
  221. $last_order->update_status( 'pending', _x( 'Subscription renewal payment retry:', 'used in order note as reason for why order status changed', 'woocommerce-subscriptions' ), true );
  222. // 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
  223. foreach ( $subscriptions as $subscription ) {
  224. $subscription->update_status( 'on-hold', _x( 'Subscription renewal payment retry:', 'used in order note as reason for why subscription status changed', 'woocommerce-subscriptions' ) );
  225. }
  226. WC_Subscriptions_Payment_Gateways::trigger_gateway_renewal_payment_hook( $last_order );
  227. // Now that we've attempted to process the payment, refresh the order
  228. $last_order = wc_get_order( wcs_get_objects_property( $last_order, 'id' ) );
  229. // if the order still needs payment, payment failed
  230. if ( $last_order->needs_payment() ) {
  231. $last_retry->update_status( 'failed' );
  232. } else {
  233. $last_retry->update_status( 'complete' );
  234. }
  235. } else {
  236. // order or subscription statuses have been manually updated, so we'll cancel the retry
  237. $last_retry->update_status( 'cancelled' );
  238. }
  239. } else {
  240. // last order must have been paid for some other way, so we'll cancel the retry
  241. $last_retry->update_status( 'cancelled' );
  242. }
  243. do_action( 'woocommerce_subscriptions_after_payment_retry', $last_retry, $last_order );
  244. }
  245. }
  246. /**
  247. * Determines if a renewal order and the last retry statuses are the same (used to determine if a payment method
  248. * change is needed)
  249. *
  250. * @since 2.2.8
  251. */
  252. public static function compare_order_and_retry_statuses( $is_failed_order, $order_id, $order_status ) {
  253. $last_retry = self::store()->get_last_retry_for_order( $order_id );
  254. if ( null !== $last_retry && $order_status === $last_retry->get_rule()->get_status_to_apply( 'order' ) ) {
  255. $is_failed_order = true;
  256. }
  257. return $is_failed_order;
  258. }
  259. /**
  260. * Access the object used to interface with the database
  261. *
  262. * @since 2.1
  263. */
  264. public static function store() {
  265. if ( empty( self::$store ) ) {
  266. $class = self::get_store_class();
  267. self::$store = new $class();
  268. }
  269. return self::$store;
  270. }
  271. /**
  272. * Get the class used for instantiating retry storage via self::store()
  273. *
  274. * @since 2.1
  275. */
  276. protected static function get_store_class() {
  277. return apply_filters( 'wcs_retry_store_class', 'WCS_Retry_Post_Store' );
  278. }
  279. /**
  280. * Setup and access the object used to interface with retry rules
  281. *
  282. * @since 2.1
  283. */
  284. public static function rules() {
  285. if ( empty( self::$retry_rules ) ) {
  286. $class = self::get_rules_class();
  287. self::$retry_rules = new $class();
  288. }
  289. return self::$retry_rules;
  290. }
  291. /**
  292. * Get the class used for instantiating retry rules via self::rules()
  293. *
  294. * @since 2.1
  295. */
  296. protected static function get_rules_class() {
  297. return apply_filters( 'wcs_retry_rules_class', 'WCS_Retry_Rules' );
  298. }
  299. }
  300. WCS_Retry_Manager::init();