PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.20
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.20
1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.12 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.54 1.3.53 1.3.52 All 184 releases
disco / functions / order.php

order.php in Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO 1.4.20, at functions/order.php

349 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Disco
4 *
5 * @package Disco
6 * @author Ohidul Islam <[email protected]>
7 * @link http://domain.tld
8 * @license GPL 2.0+
9 * @copyright 2022 WebAppick
10 */
11
12 // Ensure the file is not accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 if ( ! function_exists( 'disco_add_order_meta' ) ) {
18
19 /**
20 * Get a campaign id from WC session and set into post meta after place order.
21 * Unset WC session after update post-meta.
22 *
23 * @param int $order_id Order ID.
24 * @return void
25 */
26 function disco_add_order_meta( $order_id ) {
27 // Validate session exists
28 if ( ! WC()->session ) {
29 return;
30 }
31
32 $campaigns = WC()->session->get( 'disco_campaign' );
33
34 if ( empty( $campaigns ) || ! is_array( $campaigns ) ) {
35 return;
36 }
37
38 $order = wc_get_order( $order_id );
39
40 // Validate order exists
41 if ( ! $order instanceof WC_Order ) {
42 return;
43 }
44
45 /**
46 * Add each campaign ID as separate meta entry.
47 * Using unique=false allows multiple campaign IDs per order.
48 */
49 /**
50 * Guard against double execution.
51 *
52 * This callback fires on both woocommerce_thankyou and
53 * woocommerce_payment_complete. Depending on the gateway and page
54 * reloads either can run first, so skip if campaign meta is already
55 * stored to avoid duplicate disco_campaign entries.
56 */
57 $existing = $order->get_meta( 'disco_campaign', false );
58
59 if ( ! empty( $existing ) ) {
60 disco_finalise_campaign_usage( $order );
61
62 return;
63 }
64
65 foreach ( $campaigns as $campaign_id ) {
66 $order->add_meta_data( 'disco_campaign', (int) $campaign_id, false );
67 }
68
69 // Save once after all meta is added (more efficient)
70 $order->save();
71
72 WC()->session->__unset( 'disco_campaign' );
73
74 // Drop the cached usage counts for the campaigns this order just used.
75 foreach ( $campaigns as $campaign_id ) {
76 \Disco\App\Features\UserLimit::flush_cache( (int) $campaign_id );
77 }
78
79 // Clear price cache so user limits are re-evaluated
80 if ( !function_exists( 'disco_clear_price_cache' ) ) {
81 return;
82 }
83
84 disco_clear_price_cache();
85 }
86
87 add_action( 'woocommerce_thankyou', 'disco_add_order_meta', PHP_INT_MAX );
88 add_action( 'woocommerce_payment_complete', 'disco_add_order_meta', PHP_INT_MAX );
89 }
90
91 if ( ! function_exists( 'disco_finalise_campaign_usage' ) ) {
92
93 /**
94 * Settle the caches and session for an order that already carries its meta.
95 *
96 * Normally the meta was written by disco_add_campaign_order_meta() as the
97 * order was created. The usage caches have to be dropped here rather than
98 * there: at creation the order is not persisted yet, so a flush would
99 * immediately repopulate the old count from the database.
100 *
101 * @param \WC_Order $order Order object.
102 * @return void
103 */
104 function disco_finalise_campaign_usage( $order ) {
105 foreach ( disco_campaign_ids_on_order( $order ) as $campaign_id ) {
106 \Disco\App\Features\UserLimit::flush_cache( $campaign_id );
107 }
108
109 if ( WC()->session ) {
110 WC()->session->__unset( 'disco_campaign' );
111 }
112
113 if ( ! function_exists( 'disco_clear_price_cache' ) ) {
114 return;
115 }
116
117 disco_clear_price_cache();
118 }
119
120 }
121
122 if ( ! function_exists( 'disco_campaign_ids_on_order' ) ) {
123
124 /**
125 * Campaign IDs already recorded on an order.
126 *
127 * @param \WC_Order $order Order object.
128 * @return array<int, int>
129 */
130 function disco_campaign_ids_on_order( $order ): array {
131 if ( ! $order instanceof WC_Order ) {
132 return array();
133 }
134
135 $meta = $order->get_meta( 'disco_campaign', false );
136
137 if ( empty( $meta ) ) {
138 return array();
139 }
140
141 return array_map( 'intval', wp_list_pluck( $meta, 'value' ) );
142 }
143
144 }
145
146 if ( ! function_exists( 'disco_add_campaign_order_meta' ) ) {
147
148 /**
149 * Record the applied campaigns while the order is being created.
150 *
151 * The campaign IDs are staged in the WC session as the cart is priced, and
152 * were previously read back on woocommerce_thankyou / payment_complete. Those
153 * run in a later request, which is not always the same session: an off site
154 * gateway can return the customer on a fresh one, and a payment webhook has
155 * no customer session at all. The handover then found nothing and returned
156 * silently, leaving a discounted order with no campaign recorded, so its
157 * usage limit never counted it and Analytics never saw it.
158 *
159 * Reading the session here instead keeps both halves in the request that
160 * priced the cart. The later hooks stay in place as a fallback for anything
161 * that reaches them without meta, and to drop the usage caches once the order
162 * actually exists.
163 *
164 * @param \WC_Order $order The order being created.
165 * @return void
166 */
167 function disco_add_campaign_order_meta( $order ) {
168 if ( ! $order instanceof WC_Order || ! WC()->session ) {
169 return;
170 }
171
172 $campaigns = WC()->session->get( 'disco_campaign' );
173
174 if ( empty( $campaigns ) || ! is_array( $campaigns ) ) {
175 return;
176 }
177
178 $existing = disco_campaign_ids_on_order( $order );
179
180 foreach ( $campaigns as $campaign_id ) {
181 $campaign_id = (int) $campaign_id;
182
183 if ( in_array( $campaign_id, $existing, true ) ) {
184 continue;
185 }
186
187 $order->add_meta_data( 'disco_campaign', $campaign_id, false );
188
189 $existing[] = $campaign_id;
190 }
191 }
192
193 // Classic (shortcode) checkout: order is saved by WC after this action.
194 add_action( 'woocommerce_checkout_create_order', 'disco_add_campaign_order_meta', 10 );
195
196 // Block / Store API checkout: WC_Checkout::create_order() does not run, so
197 // the action above never fires.
198 add_action( 'woocommerce_store_api_checkout_update_order_meta', 'disco_add_campaign_order_meta', 10 );
199 }
200
201 if ( ! function_exists( 'disco_flush_campaign_usage_cache_on_status_change' ) ) {
202
203 /**
204 * Invalidate cached campaign usage counts when an order changes status.
205 *
206 * The usage count only counts orders in processing / on-hold / completed, so
207 * any status transition can change it. Flushing here keeps the cached count
208 * correct without querying on every cart or fragment-refresh request.
209 *
210 * @param int $order_id Order ID.
211 * @return void
212 */
213 function disco_flush_campaign_usage_cache_on_status_change( $order_id ) {
214 $order = wc_get_order( $order_id );
215
216 if ( ! $order instanceof WC_Order ) {
217 return;
218 }
219
220 $meta = $order->get_meta( 'disco_campaign', false );
221
222 if ( empty( $meta ) || ! is_array( $meta ) ) {
223 return;
224 }
225
226 foreach ( wp_list_pluck( $meta, 'value' ) as $campaign_id ) {
227 \Disco\App\Features\UserLimit::flush_cache( (int) $campaign_id );
228 }
229 }
230
231 add_action( 'woocommerce_order_status_changed', 'disco_flush_campaign_usage_cache_on_status_change', 10, 1 );
232 add_action( 'woocommerce_trash_order', 'disco_flush_campaign_usage_cache_on_status_change', 10, 1 );
233 add_action( 'woocommerce_untrash_order', 'disco_flush_campaign_usage_cache_on_status_change', 10, 1 );
234 add_action( 'woocommerce_before_delete_order', 'disco_flush_campaign_usage_cache_on_status_change', 10, 1 );
235 add_action( 'woocommerce_delete_order', 'disco_flush_campaign_usage_cache_on_status_change', 10, 1 );
236 }
237
238 if ( ! function_exists( 'disco_flush_campaign_usage_cache_on_meta_write' ) ) {
239
240 /**
241 * Invalidate cached campaign usage counts when disco_campaign meta is written
242 * outside of this plugin.
243 *
244 * Imports, migrations and third-party code write order meta directly rather
245 * than going through disco_add_order_meta(), so without this the cached count
246 * would stay stale until its TTL expired.
247 *
248 * @param int|array $meta_id Meta ID (array on delete).
249 * @param int $object_id Order ID.
250 * @param string $meta_key Meta key.
251 * @param mixed $meta_value Meta value.
252 * @return void
253 */
254 // $meta_id and $object_id are unused but required: the meta hooks pass their
255 // arguments positionally, so $meta_key and $meta_value cannot be reached
256 // without declaring them.
257 function disco_flush_campaign_usage_cache_on_meta_write( $meta_id, $object_id, $meta_key, $meta_value ) { //phpcs:ignore
258 if ( 'disco_campaign' !== $meta_key ) {
259 return;
260 }
261
262 \Disco\App\Features\UserLimit::flush_cache( (int) $meta_value );
263 }
264
265 // Legacy (post table) orders.
266 add_action( 'added_post_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 );
267 add_action( 'updated_post_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 );
268 add_action( 'deleted_post_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 );
269
270 // HPOS orders.
271 add_action( 'added_wc_order_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 );
272 add_action( 'updated_wc_order_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 );
273 add_action( 'deleted_wc_order_meta', 'disco_flush_campaign_usage_cache_on_meta_write', 10, 4 );
274 }
275
276 if ( ! function_exists( 'disco_reset_campaign_session' ) ) {
277
278 /**
279 * Clear the disco_campaign session before cart totals are recalculated.
280 *
281 * This prevents stale campaign IDs (from previously discounted products
282 * that were later removed from the cart) from being saved to order meta.
283 * The session is rebuilt fresh each time checkout prices are recalculated.
284 *
285 * @return void
286 */
287 function disco_reset_campaign_session() {
288 if ( ! is_checkout() ) {
289 return;
290 }
291
292 if ( ! WC()->session ) {
293 return;
294 }
295
296 WC()->session->__unset( 'disco_campaign' );
297 }
298
299 add_action( 'woocommerce_before_calculate_totals', 'disco_reset_campaign_session', 0 );
300 }
301
302 if ( ! function_exists( 'disco_add_free_shipping_order_meta' ) ) {
303
304 /**
305 * Persist free-shipping campaign IDs to the order meta during checkout.
306 *
307 * Free shipping is applied through the woocommerce_package_rates filter,
308 * whose results WooCommerce caches per package. Because of that cache, the
309 * filter is not guaranteed to run on the final order-placement recalculation,
310 * so the campaign ID cannot be reliably staged in the WC session like the
311 * product and cart intents are. Instead we evaluate the Shipping intents
312 * fresh here, where the order object and cart are both available, and write
313 * the campaign IDs straight to the order.
314 *
315 * @param \WC_Order $order The order being created.
316 * @return void
317 */
318 function disco_add_free_shipping_order_meta( $order ) {
319 if ( ! $order instanceof WC_Order ) {
320 return;
321 }
322
323 $campaign_ids = ( new \Disco\App\Disco )->get_applied_free_shipping_campaign_ids();
324
325 if ( empty( $campaign_ids ) ) {
326 return;
327 }
328
329 // Avoid duplicating IDs already staged from product/cart intents.
330 $existing = array_map( 'intval', $order->get_meta( 'disco_campaign', false ) ? wp_list_pluck( $order->get_meta( 'disco_campaign', false ), 'value' ) : array() );
331
332 foreach ( $campaign_ids as $campaign_id ) {
333 if ( in_array( (int) $campaign_id, $existing, true ) ) {
334 continue;
335 }
336
337 $order->add_meta_data( 'disco_campaign', (int) $campaign_id, false );
338 }
339 }
340
341 // Classic (shortcode) checkout: order is saved by WC after this action.
342 add_action( 'woocommerce_checkout_create_order', 'disco_add_free_shipping_order_meta', 20 );
343
344 // Block / Store API checkout: WC_Checkout::create_order() does not run, so
345 // the action above never fires. This Store API hook also passes the WC_Order
346 // (as its first arg) before the order is persisted.
347 add_action( 'woocommerce_store_api_checkout_update_order_meta', 'disco_add_free_shipping_order_meta', 20 );
348 }
349