PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.4.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.4.0
2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 All 65 releases
subscription / includes / Illuminate / StockPause.php

StockPause.php in Subscriptions for WooCommerce with Stripe Recurring Payments 2.4.0, at includes/Illuminate/StockPause.php

246 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SpringDevs\Subscription\Illuminate;
4
5 use WC_Product;
6
7 /**
8 * Class StockPause
9 *
10 * Pauses (`on_hold`) active subscriptions whose product has run low on stock and
11 * resumes them once stock recovers, so a renewal never charges a customer for
12 * something the store cannot ship.
13 *
14 * Opt-in via the `wp_subscription_stock_pause` setting. Only subscriptions this
15 * class paused (marked with `_subscrpt_stock_paused`) are ever resumed here, so a
16 * subscription an admin put on hold by hand stays on hold.
17 *
18 * @package SpringDevs\Subscription\Illuminate
19 */
20 class StockPause {
21
22 /**
23 * Meta set on a subscription paused because of stock.
24 */
25 const META_KEY = '_subscrpt_stock_paused';
26
27 /**
28 * Initialize the class.
29 */
30 public function __construct() {
31 add_action( 'woocommerce_product_set_stock', [ $this, 'on_stock_changed' ] );
32 add_action( 'woocommerce_variation_set_stock', [ $this, 'on_stock_changed' ] );
33 add_action( 'woocommerce_product_set_stock_status', [ $this, 'on_stock_status_changed' ], 10, 3 );
34 add_action( 'woocommerce_variation_set_stock_status', [ $this, 'on_stock_status_changed' ], 10, 3 );
35 }
36
37 /**
38 * Whether the feature is switched on.
39 *
40 * @return bool
41 */
42 public static function is_enabled(): bool {
43 return in_array( get_option( 'wp_subscription_stock_pause', '' ), [ 1, '1', 'true', 'yes' ], true );
44 }
45
46 /**
47 * Stock level at or below which subscriptions pause.
48 *
49 * @return int
50 */
51 public static function get_threshold(): int {
52 return max( 0, (int) get_option( 'wp_subscription_stock_pause_threshold', 0 ) );
53 }
54
55 /**
56 * Stock quantity changed.
57 *
58 * @param WC_Product $product Product (or variation).
59 */
60 public function on_stock_changed( $product ) {
61 $this->sync( $product );
62 }
63
64 /**
65 * Stock status changed.
66 *
67 * @param int $product_id Product id.
68 * @param string $stock_status New stock status.
69 * @param WC_Product $product Product (or variation).
70 */
71 public function on_stock_status_changed( $product_id, $stock_status, $product = null ) {
72 $this->sync( $product ? $product : wc_get_product( $product_id ) );
73 }
74
75 /**
76 * Whether a product is at or below the pause threshold.
77 *
78 * Products that do not manage a quantity only pause when out of stock.
79 *
80 * @param WC_Product $product Product (or variation).
81 * @return bool
82 */
83 public static function is_low_stock( $product ): bool {
84 if ( 'outofstock' === $product->get_stock_status() ) {
85 return true;
86 }
87
88 if ( $product->managing_stock() && null !== $product->get_stock_quantity() ) {
89 return (int) $product->get_stock_quantity() <= self::get_threshold();
90 }
91
92 return false;
93 }
94
95 /**
96 * Pause or resume the subscriptions of a product to match its stock.
97 *
98 * @param WC_Product|false|null $product Product (or variation).
99 */
100 public function sync( $product ) {
101 if ( ! $product instanceof WC_Product || ! self::is_enabled() ) {
102 return;
103 }
104
105 if ( self::is_low_stock( $product ) ) {
106 $this->pause( $product );
107 } elseif ( '1' === get_option( 'wp_subscription_stock_auto_resume', '1' ) ) {
108 $this->resume( $product );
109 }
110 }
111
112 /**
113 * Subscription ids of a product (or variation) in the given statuses.
114 *
115 * @param WC_Product $product Product (or variation).
116 * @param string[] $statuses Subscription statuses.
117 * @param array $extra Extra meta_query clauses.
118 * @return int[]
119 */
120 private function get_subscriptions( WC_Product $product, array $statuses, array $extra = [] ): array {
121 $is_variation = $product->is_type( 'variation' );
122
123 $meta_query = [
124 [
125 'key' => '_subscrpt_product_id',
126 'value' => $is_variation ? $product->get_parent_id() : $product->get_id(),
127 ],
128 ];
129 if ( $is_variation ) {
130 $meta_query[] = [
131 'key' => '_subscrpt_variation_id',
132 'value' => $product->get_id(),
133 ];
134 }
135
136 return array_map(
137 'intval',
138 get_posts(
139 [
140 'post_type' => 'subscrpt_order',
141 'post_status' => $statuses,
142 'fields' => 'ids',
143 'posts_per_page' => -1,
144 'meta_query' => array_merge( $meta_query, $extra ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
145 ]
146 )
147 );
148 }
149
150 /**
151 * Pause the active subscriptions of a product and tell their customers.
152 *
153 * @param WC_Product $product Product (or variation).
154 */
155 private function pause( WC_Product $product ) {
156 foreach ( $this->get_subscriptions( $product, [ 'active' ] ) as $subscription_id ) {
157 Action::status( 'on_hold', $subscription_id, false );
158 update_post_meta( $subscription_id, self::META_KEY, time() );
159
160 $this->add_note(
161 $subscription_id,
162 __( 'Subscription paused because the product is low on stock.', 'subscription' ),
163 'Subscription Paused (Low Stock)',
164 'subs_stock_paused'
165 );
166
167 if ( function_exists( 'WC' ) && WC()->mailer() ) {
168 do_action( 'subscrpt_subscription_stock_paused_email_notification', $subscription_id );
169 }
170
171 /**
172 * Fires after a subscription is paused because of low stock.
173 *
174 * @param int $subscription_id Subscription id.
175 * @param WC_Product $product Product (or variation) that is low on stock.
176 */
177 do_action( 'subscrpt_subscription_stock_paused', $subscription_id, $product );
178 }
179 }
180
181 /**
182 * Resume the subscriptions this class paused for a product.
183 *
184 * @param WC_Product $product Product (or variation).
185 */
186 private function resume( WC_Product $product ) {
187 $paused = $this->get_subscriptions(
188 $product,
189 [ 'on_hold', 'active', 'pending', 'cancelled', 'pe_cancelled', 'expired', 'completed' ],
190 [
191 [
192 'key' => self::META_KEY,
193 'compare' => 'EXISTS',
194 ],
195 ]
196 );
197
198 foreach ( $paused as $subscription_id ) {
199 delete_post_meta( $subscription_id, self::META_KEY );
200
201 // Changed by hand since: nothing to resume.
202 if ( 'on_hold' !== get_post_status( $subscription_id ) ) {
203 continue;
204 }
205
206 Action::status( 'active', $subscription_id, false );
207
208 $this->add_note(
209 $subscription_id,
210 __( 'Subscription resumed because the product is back in stock.', 'subscription' ),
211 'Subscription Resumed (Back In Stock)',
212 'subs_stock_resumed'
213 );
214
215 /**
216 * Fires after a subscription is resumed because stock recovered.
217 *
218 * @param int $subscription_id Subscription id.
219 * @param WC_Product $product Product (or variation) back in stock.
220 */
221 do_action( 'subscrpt_subscription_stock_resumed', $subscription_id, $product );
222 }
223 }
224
225 /**
226 * Write an activity note on a subscription.
227 *
228 * @param int $subscription_id Subscription id.
229 * @param string $content Note text.
230 * @param string $activity Activity label.
231 * @param string $type Activity type.
232 */
233 private function add_note( int $subscription_id, string $content, string $activity, string $type ) {
234 $comment_id = wp_insert_comment(
235 [
236 'comment_author' => 'Subscription for WooCommerce',
237 'comment_content' => $content,
238 'comment_post_ID' => $subscription_id,
239 'comment_type' => 'order_note',
240 ]
241 );
242 update_comment_meta( $comment_id, '_subscrpt_activity', $activity );
243 update_comment_meta( $comment_id, '_subscrpt_activity_type', $type );
244 }
245 }
246