PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.14
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.14
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 1.3.51 1.3.50 1.3.49 1.3.48 1.3.47 1.3.46 1.3.45 All 177 releases
disco / app / Features / UserLimit.php

UserLimit.php in Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO 1.4.14, at app/Features/UserLimit.php

255 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * UserLimit Feature
4 *
5 * @package Disco
6 * @subpackage App\Utility
7 * @since 1.0.0
8 * @category Utility
9 */
10
11 namespace Disco\App\Features;
12
13 /**
14 * Class DiscountLimit
15 *
16 * @package Disco
17 * @subpackage App\Feature
18 * @author Ohidul Islam <wahid0003@gmail.com>
19 * @link https://webappick.com
20 * @license https://opensource.org/licenses/gpl-license.php GNU Public License
21 * @category Utility
22 */
23 class UserLimit {
24
25 /**
26 * Object cache group for campaign usage counts.
27 */
28 public const CACHE_GROUP = 'disco_user_limit';
29
30 /**
31 * Transient / cache key prefix for campaign usage counts.
32 */
33 public const CACHE_KEY_PREFIX = 'disco_campaign_usage_';
34
35 /**
36 * Default cache lifetime, in seconds, for campaign usage counts.
37 */
38 public const CACHE_TTL = 300;
39
40 /**
41 * Disco start session function.
42 * Set campaign in WC session as an array.
43 *
44 * @param int $campaign_id Campaign ID.
45 * @return void
46 */
47 public function disco_start_session_on_checkout( $campaign_id ) {
48 if ( ! is_checkout() ) {
49 return;
50 }
51
52 // Retrieve the current associative array from the session
53 $disco_campaign = WC()->session->get( 'disco_campaign', array() );
54
55 if ( !is_array( $disco_campaign ) ) {
56 $disco_campaign = (array) $disco_campaign;
57 }
58
59 // Check if the key (campaign ID) already exists
60 if ( in_array( $campaign_id, $disco_campaign, true ) ) {
61 return;
62 }
63
64 // Add the new data using the campaign ID as the key
65 $disco_campaign[] = $campaign_id;
66
67 // Save the updated associative array back to the session
68 WC()->session->set( 'disco_campaign', $disco_campaign );
69 }
70
71 /**
72 * Whether a campaign has reached its configured global usage limit.
73 *
74 * This is the entry point callers should use instead of fetching the count
75 * directly: when a campaign has no usage limit configured (the common case)
76 * it returns early and no order/meta query is issued at all.
77 *
78 * @param \Disco\App\Utility\Config|object $campaign Campaign config object.
79 * @return bool
80 */
81 public function disco_is_limit_reached( $campaign ) {
82 if ( ! $campaign instanceof \Disco\App\Utility\Config || empty( $campaign->id ) ) {
83 return false;
84 }
85
86 $limit = $campaign->discount_max_user;
87
88 // No limit configured — never query.
89 if ( empty( $limit ) || ! is_numeric( $limit ) || (int) $limit < 0 ) {
90 return false;
91 }
92
93 return $this->disco_get_total_applied_campaign( (int) $campaign->id ) >= (int) $limit;
94 }
95
96 /**
97 * Retrieve total applied campaign by campaign id.
98 *
99 * The result is memoized per request and cached (object cache + transient)
100 * because the underlying COUNT() joins the order and order-meta tables,
101 * which is expensive on large stores. Usage limits do not need real-time
102 * precision at cart/fragment-refresh frequency; the cache is invalidated
103 * when an order gains campaign meta or changes status.
104 *
105 * @param int $campaign_id Campaign ID.
106 * @return int
107 */
108 public function disco_get_total_applied_campaign( $campaign_id ) {
109 $campaign_id = (int) $campaign_id;
110 $cache_key = self::CACHE_KEY_PREFIX . $campaign_id;
111 $cached = wp_cache_get( $cache_key, self::CACHE_GROUP );
112
113 if ( false === $cached ) {
114 $cached = get_transient( $cache_key );
115 }
116
117 if ( is_numeric( $cached ) ) {
118 return (int) $cached;
119 }
120
121 $count = $this->query_total_applied_campaign( $campaign_id );
122
123 /**
124 * Filter the cache lifetime, in seconds, of campaign usage counts.
125 *
126 * @param int $ttl Lifetime in seconds.
127 * @param int $campaign_id Campaign ID.
128 */
129 $ttl = (int) apply_filters( 'disco_campaign_usage_cache_ttl', self::CACHE_TTL, $campaign_id );
130
131 if ( $ttl > 0 ) {
132 wp_cache_set( $cache_key, $count, self::CACHE_GROUP, $ttl );
133 set_transient( $cache_key, $count, $ttl );
134 }
135
136 return $count;
137 }
138
139 /**
140 * Invalidate the cached usage count for a campaign, or for all campaigns.
141 *
142 * @param int|null $campaign_id Campaign ID, or null to flush every campaign.
143 * @return void
144 */
145 public static function flush_cache( $campaign_id = null ) {
146 if ( null !== $campaign_id ) {
147 $campaign_id = (int) $campaign_id;
148 $cache_key = self::CACHE_KEY_PREFIX . $campaign_id;
149
150 wp_cache_delete( $cache_key, self::CACHE_GROUP );
151 delete_transient( $cache_key );
152
153 return;
154 }
155
156 if ( ! class_exists( \Disco\App\Campaign::class ) ) {
157 return;
158 }
159
160 $campaigns = ( new \Disco\App\Campaign )->get_rows();
161
162 if ( ! is_array( $campaigns ) ) {
163 return;
164 }
165
166 foreach ( array_keys( $campaigns ) as $id ) {
167 self::flush_cache( $id );
168 }
169 }
170
171 /**
172 * Run the usage count query against the order tables.
173 *
174 * The order-meta table is joined first (via STRAIGHT_JOIN) so MySQL resolves
175 * the small `meta_key`/`meta_value` set before touching the orders table.
176 * Letting the optimizer start from the orders table means scanning every
177 * order on the store, which is what made this query pathologically slow.
178 *
179 * @param int $campaign_id Campaign ID.
180 * @return int
181 */
182 private function query_total_applied_campaign( $campaign_id ) { // phpcs:disable
183 global $wpdb;
184
185 // Filter the order statuses
186 $status = apply_filters(
187 'disco_campaign_user_limit_order_statuses',
188 array(
189 'wc-processing',
190 'wc-on-hold',
191 'wc-completed',
192 ),
193 $campaign_id
194 );
195
196 $placeholders = implode( ',', array_fill( 0, count( $status ), '%s' ) );
197
198 // Check HPOS first
199 if ( $this->disco_is_hpos_enabled() ) {
200 $sql = "
201 SELECT STRAIGHT_JOIN COUNT(DISTINCT om.order_id)
202 FROM {$wpdb->prefix}wc_orders_meta AS om
203 INNER JOIN {$wpdb->prefix}wc_orders AS o ON o.id = om.order_id
204 WHERE om.meta_key = %s
205 AND om.meta_value = %s
206 AND o.type = 'shop_order'
207 AND o.status IN ($placeholders)
208 ";
209
210 $count = $wpdb->get_var(
211 $wpdb->prepare(
212 $sql,
213 ...array_merge( array( 'disco_campaign', (string) $campaign_id ), $status )
214 )
215 );
216
217 return (int) $count;
218 }
219
220 // Legacy query for posts table (non-HPOS only)
221 $sql = "
222 SELECT STRAIGHT_JOIN COUNT(DISTINCT pm.post_id)
223 FROM {$wpdb->postmeta} AS pm
224 INNER JOIN {$wpdb->posts} AS p ON p.ID = pm.post_id
225 WHERE pm.meta_key = %s
226 AND pm.meta_value = %s
227 AND p.post_type = 'shop_order'
228 AND p.post_status IN ($placeholders)
229 ";
230
231 $count = $wpdb->get_var(
232 $wpdb->prepare(
233 $sql,
234 ...array_merge( array( 'disco_campaign', (string) $campaign_id ), $status )
235 )
236 );
237
238 return (int) $count;
239 }
240
241 /**
242 * Check WooCommerce HPOS is enabled or not.
243 *
244 * @return bool
245 */
246 private function disco_is_hpos_enabled() {
247 if ( class_exists( \Automattic\WooCommerce\Utilities\OrderUtil::class ) ) {
248 return \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
249 }
250
251 return false;
252 }
253
254 }
255