PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.78
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.78
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / wishlist / Wishlist_Service.php

Wishlist_Service.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.78, at includes/wishlist/Wishlist_Service.php

766 lines 24.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace King_Addons\Wishlist;
4
5 use DateTime;
6 use DateTimeZone;
7 use WP_Error;
8 use wpdb;
9
10 if (!defined('ABSPATH')) {
11 exit;
12 }
13
14 /**
15 * Provides CRUD operations for wishlist items and lists.
16 */
17 class Wishlist_Service
18 {
19 private const DEFAULT_WISHLIST_ID = 'default';
20 private const CACHE_TTL = 600;
21
22 private Wishlist_Session $session;
23 private string $session_key;
24 private int $user_id;
25 private string $active_wishlist_id;
26
27 /**
28 * Set up wishlist service with current user and session.
29 *
30 * @param int|null $user_id Optional user identifier.
31 * @param string|null $session_key Optional session key for guests.
32 */
33 public function __construct(?int $user_id = null, ?string $session_key = null)
34 {
35 $this->session = new Wishlist_Session();
36 $this->user_id = $user_id ?? get_current_user_id();
37 $this->session_key = $session_key ?: $this->session->get_session_key();
38 $this->active_wishlist_id = $this->resolve_active_wishlist_id();
39 }
40
41 /**
42 * Get current active wishlist identifier.
43 *
44 * @return string Active wishlist id.
45 */
46 public function get_active_wishlist_id(): string
47 {
48 return $this->active_wishlist_id;
49 }
50
51 /**
52 * Set current active wishlist identifier.
53 *
54 * @param string $wishlist_id Wishlist identifier.
55 * @return void
56 */
57 public function set_active_wishlist_id(string $wishlist_id): void
58 {
59 $this->active_wishlist_id = $this->normalize_wishlist_id($wishlist_id);
60 if ($this->user_id > 0) {
61 update_user_meta($this->user_id, 'king_addons_active_wishlist_id', $this->active_wishlist_id);
62 }
63 }
64
65 /**
66 * Add product to wishlist.
67 *
68 * @param int $product_id Product identifier.
69 * @param int $variation_id Variation identifier.
70 * @param int $qty Quantity to store.
71 * @param string|null $wishlist_id Wishlist identifier.
72 * @return array|WP_Error Operation result.
73 */
74 public function add_item(int $product_id, int $variation_id = 0, int $qty = 1, ?string $wishlist_id = null)
75 {
76 $validation = $this->validate_product($product_id, $variation_id);
77 if (is_wp_error($validation)) {
78 return $validation;
79 }
80
81 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
82 $qty = max(1, $qty);
83 $now = $this->now();
84
85 global $wpdb;
86 $table = Wishlist_DB::get_items_table();
87
88 $existing = $this->get_item_row($wishlist_id, $product_id, $variation_id);
89
90 if ($existing) {
91 $wpdb->update(
92 $table,
93 [
94 'qty' => $qty,
95 'updated_at' => $now,
96 ],
97 [
98 'id' => intval($existing->id),
99 ],
100 ['%d', '%s'],
101 ['%d']
102 );
103 } else {
104 $wpdb->insert(
105 $table,
106 [
107 'user_id' => $this->user_id,
108 'session_key' => $this->session_key,
109 'wishlist_id' => $wishlist_id,
110 'product_id' => $product_id,
111 'variation_id' => $variation_id,
112 'qty' => $qty,
113 'created_at' => $now,
114 'updated_at' => $now,
115 'meta' => null,
116 ],
117 ['%d', '%s', '%s', '%d', '%d', '%d', '%s', '%s', '%s']
118 );
119 }
120
121 $this->ensure_default_list($wishlist_id);
122 $this->invalidate_cache($wishlist_id);
123
124 return [
125 'success' => true,
126 'wishlist_id' => $wishlist_id,
127 'count' => $this->get_count($wishlist_id, true),
128 ];
129 }
130
131 /**
132 * Remove a product from wishlist.
133 *
134 * @param int $product_id Product identifier.
135 * @param int $variation_id Variation identifier.
136 * @param string|null $wishlist_id Wishlist identifier.
137 * @return array Operation result.
138 */
139 public function remove_item(int $product_id, int $variation_id = 0, ?string $wishlist_id = null): array
140 {
141 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
142 $row = $this->get_item_row($wishlist_id, $product_id, $variation_id);
143
144 if (!$row) {
145 return [
146 'success' => false,
147 'message' => esc_html__('Item not found in wishlist.', 'king-addons'),
148 'count' => $this->get_count($wishlist_id, true),
149 ];
150 }
151
152 global $wpdb;
153 $wpdb->delete(
154 Wishlist_DB::get_items_table(),
155 ['id' => intval($row->id)],
156 ['%d']
157 );
158
159 $this->invalidate_cache($wishlist_id);
160
161 return [
162 'success' => true,
163 'wishlist_id' => $wishlist_id,
164 'count' => $this->get_count($wishlist_id, true),
165 ];
166 }
167
168 /**
169 * Toggle wishlist state for a product.
170 *
171 * @param int $product_id Product identifier.
172 * @param int $variation_id Variation identifier.
173 * @param int $qty Quantity to store.
174 * @param string|null $wishlist_id Wishlist identifier.
175 * @return array|WP_Error Operation result.
176 */
177 public function toggle_item(int $product_id, int $variation_id = 0, int $qty = 1, ?string $wishlist_id = null)
178 {
179 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
180 $existing = $this->get_item_row($wishlist_id, $product_id, $variation_id);
181
182 if ($existing) {
183 return $this->remove_item($product_id, $variation_id, $wishlist_id);
184 }
185
186 return $this->add_item($product_id, $variation_id, $qty, $wishlist_id);
187 }
188
189 /**
190 * Retrieve wishlist items for current user or session.
191 *
192 * @param string|null $wishlist_id Wishlist identifier.
193 * @return array<int, object> List of wishlist rows.
194 */
195 public function get_items(?string $wishlist_id = null): array
196 {
197 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
198 global $wpdb;
199
200 $where = $this->get_scope_where($wishlist_id);
201 $table = Wishlist_DB::get_items_table();
202 $query = "SELECT * FROM {$table} WHERE {$where['sql']} ORDER BY created_at DESC";
203
204 /** @var array<int, object> $items */
205 $items = $wpdb->get_results($wpdb->prepare($query, $where['params']));
206
207 return $items ?: [];
208 }
209
210 /**
211 * Determine if wishlist already contains a product.
212 *
213 * @param int $product_id Product identifier.
214 * @param int $variation_id Variation identifier.
215 * @param string|null $wishlist_id Wishlist identifier.
216 * @return bool Whether the item is present.
217 */
218 public function has_item(int $product_id, int $variation_id = 0, ?string $wishlist_id = null): bool
219 {
220 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
221 return (bool) $this->get_item_row($wishlist_id, $product_id, $variation_id);
222 }
223
224 /**
225 * Get available wishlists for current scope.
226 *
227 * @return array<int, object> Lists rows.
228 */
229 public function get_lists(): array
230 {
231 global $wpdb;
232
233 $lists_table = Wishlist_DB::get_lists_table();
234 $where = $this->user_id > 0
235 ? $wpdb->prepare('user_id = %d', $this->user_id)
236 : $wpdb->prepare('session_key = %s', $this->session_key);
237
238 $lists = $wpdb->get_results("SELECT * FROM {$lists_table} WHERE {$where} ORDER BY created_at DESC");
239
240 if (empty($lists)) {
241 $this->ensure_default_list(self::DEFAULT_WISHLIST_ID);
242 $lists = $wpdb->get_results("SELECT * FROM {$lists_table} WHERE {$where} ORDER BY created_at DESC");
243 }
244
245 return $lists ?: [];
246 }
247
248 /**
249 * Create a new wishlist record.
250 *
251 * @param string $title Wishlist title.
252 * @param string $visibility Visibility mode.
253 * @return array<string, mixed>|WP_Error Created list data or error.
254 */
255 public function create_list(string $title, string $visibility = 'private')
256 {
257 global $wpdb;
258
259 if (empty($title)) {
260 return new WP_Error('wishlist_title_missing', esc_html__('List title is required.', 'king-addons'));
261 }
262
263 $slug_base = sanitize_title($title);
264 $slug = $slug_base ?: 'list-' . wp_generate_uuid4();
265 $lists_table = Wishlist_DB::get_lists_table();
266 $counter = 1;
267
268 while ($wpdb->get_var($wpdb->prepare("SELECT id FROM {$lists_table} WHERE slug = %s", $slug))) {
269 $slug = $slug_base . '-' . $counter;
270 ++$counter;
271 }
272
273 $now = $this->now();
274 $wpdb->insert(
275 $lists_table,
276 [
277 'user_id' => $this->user_id,
278 'session_key' => $this->user_id > 0 ? '' : $this->session_key,
279 'title' => $title,
280 'slug' => $slug,
281 'visibility' => in_array($visibility, ['private', 'shared', 'public'], true) ? $visibility : 'private',
282 'created_at' => $now,
283 'updated_at' => $now,
284 ],
285 ['%d', '%s', '%s', '%s', '%s', '%s', '%s']
286 );
287
288 return [
289 'id' => $wpdb->insert_id,
290 'slug' => $slug,
291 'title' => $title,
292 'visibility' => $visibility,
293 ];
294 }
295
296 /**
297 * Update item note stored in meta JSON.
298 *
299 * @param int $product_id Product identifier.
300 * @param int $variation_id Variation identifier.
301 * @param string $note Note content.
302 * @param string|null $wishlist_id Wishlist identifier.
303 * @return bool|WP_Error Whether update succeeded.
304 */
305 public function update_item_note(int $product_id, int $variation_id, string $note, ?string $wishlist_id = null)
306 {
307 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
308 $row = $this->get_item_row($wishlist_id, $product_id, $variation_id);
309
310 if (!$row) {
311 return new WP_Error('wishlist_note_missing_item', esc_html__('Item not found for note update.', 'king-addons'));
312 }
313
314 $meta = [];
315 if (!empty($row->meta)) {
316 $decoded = json_decode($row->meta, true);
317 if (is_array($decoded)) {
318 $meta = $decoded;
319 }
320 }
321
322 $meta['note'] = wp_strip_all_tags(wp_trim_words($note, 100));
323
324 global $wpdb;
325 $wpdb->update(
326 Wishlist_DB::get_items_table(),
327 [
328 'meta' => wp_json_encode($meta),
329 'updated_at' => $this->now(),
330 ],
331 ['id' => intval($row->id)],
332 ['%s', '%s'],
333 ['%d']
334 );
335
336 return true;
337 }
338
339 /**
340 * Get aggregated wishlist stats by product with optional date filtering.
341 *
342 * @param string|null $date_from Start date (Y-m-d format).
343 * @param string|null $date_to End date (Y-m-d format).
344 * @return array<int, array<string, mixed>> Stats per product.
345 */
346 public function get_product_stats(?string $date_from = null, ?string $date_to = null): array
347 {
348 global $wpdb;
349 $items_table = Wishlist_DB::get_items_table();
350 $conversions_table = Wishlist_DB::get_conversions_table();
351
352 $where_clauses = [];
353 $params = [];
354
355 if ($date_from) {
356 $where_clauses[] = 'i.created_at >= %s';
357 $params[] = $date_from . ' 00:00:00';
358 }
359
360 if ($date_to) {
361 $where_clauses[] = 'i.created_at <= %s';
362 $params[] = $date_to . ' 23:59:59';
363 }
364
365 $where_sql = !empty($where_clauses) ? 'WHERE ' . implode(' AND ', $where_clauses) : '';
366
367 // Build the query with conversion stats
368 $query = "
369 SELECT
370 i.product_id,
371 COUNT(DISTINCT i.id) as adds,
372 COALESCE(c.conversions, 0) as conversions,
373 COALESCE(c.revenue, 0) as revenue
374 FROM {$items_table} i
375 LEFT JOIN (
376 SELECT
377 product_id,
378 COUNT(DISTINCT order_id) as conversions,
379 SUM(order_item_total) as revenue
380 FROM {$conversions_table}
381 GROUP BY product_id
382 ) c ON i.product_id = c.product_id
383 {$where_sql}
384 GROUP BY i.product_id
385 ORDER BY adds DESC
386 LIMIT 100
387 ";
388
389 if (!empty($params)) {
390 $results = $wpdb->get_results($wpdb->prepare($query, $params), ARRAY_A);
391 } else {
392 $results = $wpdb->get_results($query, ARRAY_A);
393 }
394
395 return $results ?: [];
396 }
397
398 /**
399 * Get total wishlist statistics summary.
400 *
401 * @param string|null $date_from Start date (Y-m-d format).
402 * @param string|null $date_to End date (Y-m-d format).
403 * @return array<string, mixed> Summary stats.
404 */
405 public function get_stats_summary(?string $date_from = null, ?string $date_to = null): array
406 {
407 global $wpdb;
408 $items_table = Wishlist_DB::get_items_table();
409 $conversions_table = Wishlist_DB::get_conversions_table();
410
411 $where_items = '';
412 $where_conv = '';
413 $params_items = [];
414 $params_conv = [];
415
416 if ($date_from) {
417 $where_items .= ($where_items ? ' AND ' : 'WHERE ') . 'created_at >= %s';
418 $params_items[] = $date_from . ' 00:00:00';
419 $where_conv .= ($where_conv ? ' AND ' : 'WHERE ') . 'converted_at >= %s';
420 $params_conv[] = $date_from . ' 00:00:00';
421 }
422
423 if ($date_to) {
424 $where_items .= ($where_items ? ' AND ' : 'WHERE ') . 'created_at <= %s';
425 $params_items[] = $date_to . ' 23:59:59';
426 $where_conv .= ($where_conv ? ' AND ' : 'WHERE ') . 'converted_at <= %s';
427 $params_conv[] = $date_to . ' 23:59:59';
428 }
429
430 // Total adds
431 $adds_query = "SELECT COUNT(*) FROM {$items_table} {$where_items}";
432 $total_adds = !empty($params_items)
433 ? (int) $wpdb->get_var($wpdb->prepare($adds_query, $params_items))
434 : (int) $wpdb->get_var($adds_query);
435
436 // Total conversions and revenue
437 $conv_query = "SELECT COUNT(DISTINCT order_id) as conversions, COALESCE(SUM(order_item_total), 0) as revenue FROM {$conversions_table} {$where_conv}";
438 $conv_row = !empty($params_conv)
439 ? $wpdb->get_row($wpdb->prepare($conv_query, $params_conv), ARRAY_A)
440 : $wpdb->get_row($conv_query, ARRAY_A);
441
442 $total_conversions = (int) ($conv_row['conversions'] ?? 0);
443 $total_revenue = (float) ($conv_row['revenue'] ?? 0);
444
445 // Unique users
446 $users_query = "SELECT COUNT(DISTINCT user_id) FROM {$items_table} WHERE user_id > 0 {$where_items}";
447 // Fix the WHERE clause for users query
448 if ($where_items) {
449 $users_query = str_replace('WHERE user_id > 0 WHERE', 'WHERE user_id > 0 AND', $users_query);
450 }
451 $unique_users = !empty($params_items)
452 ? (int) $wpdb->get_var($wpdb->prepare($users_query, $params_items))
453 : (int) $wpdb->get_var($users_query);
454
455 return [
456 'total_adds' => $total_adds,
457 'total_conversions' => $total_conversions,
458 'total_revenue' => $total_revenue,
459 'unique_users' => $unique_users,
460 'conversion_rate' => $total_adds > 0 ? round(($total_conversions / $total_adds) * 100, 2) : 0,
461 ];
462 }
463
464 /**
465 * Get wishlist items count.
466 *
467 * @param string|null $wishlist_id Wishlist identifier.
468 * @param bool $force_refresh Skip cache.
469 * @return int Count of items.
470 */
471 public function get_count(?string $wishlist_id = null, bool $force_refresh = false): int
472 {
473 $wishlist_id = $this->normalize_wishlist_id($wishlist_id);
474 $cache_key = $this->get_cache_key($wishlist_id);
475 $cache_enabled = Wishlist_Settings::get('cache_enabled', false);
476 $cache_ttl = max(0, intval(Wishlist_Settings::get('cache_ttl', self::CACHE_TTL)));
477
478 if (!$force_refresh && $cache_enabled) {
479 $cached = get_transient($cache_key);
480 if (false !== $cached) {
481 return intval($cached);
482 }
483 }
484
485 global $wpdb;
486 $where = $this->get_scope_where($wishlist_id);
487 $table = Wishlist_DB::get_items_table();
488 $query = "SELECT COUNT(id) FROM {$table} WHERE {$where['sql']}";
489 $count = intval($wpdb->get_var($wpdb->prepare($query, $where['params'])));
490
491 if ($cache_enabled) {
492 set_transient($cache_key, $count, $cache_ttl ?: self::CACHE_TTL);
493 }
494
495 return $count;
496 }
497
498 /**
499 * Merge guest wishlist items into a user account on login.
500 *
501 * @param int $user_id User identifier.
502 * @param string $session_key Guest session key.
503 * @return void
504 */
505 public function merge_guest_items(int $user_id, string $session_key): void
506 {
507 if ($user_id <= 0 || empty($session_key)) {
508 return;
509 }
510
511 global $wpdb;
512 $items_table = Wishlist_DB::get_items_table();
513 $lists_table = Wishlist_DB::get_lists_table();
514
515 $guest_items = $wpdb->get_results(
516 $wpdb->prepare(
517 "SELECT * FROM {$items_table} WHERE session_key = %s",
518 $session_key
519 )
520 );
521
522 foreach ($guest_items as $item) {
523 $wishlist_id = $this->normalize_wishlist_id($item->wishlist_id);
524 $existing = $wpdb->get_var(
525 $wpdb->prepare(
526 "SELECT id FROM {$items_table} WHERE user_id = %d AND wishlist_id = %s AND product_id = %d AND variation_id = %d",
527 $user_id,
528 $wishlist_id,
529 $item->product_id,
530 $item->variation_id
531 )
532 );
533
534 if ($existing) {
535 $wpdb->update(
536 $items_table,
537 [
538 'qty' => max(intval($item->qty), 1),
539 'updated_at' => $this->now(),
540 ],
541 ['id' => intval($existing)],
542 ['%d', '%s'],
543 ['%d']
544 );
545 } else {
546 $wpdb->insert(
547 $items_table,
548 [
549 'user_id' => $user_id,
550 'session_key' => '',
551 'wishlist_id' => $wishlist_id,
552 'product_id' => intval($item->product_id),
553 'variation_id' => intval($item->variation_id),
554 'qty' => max(intval($item->qty), 1),
555 'created_at' => $this->now(),
556 'updated_at' => $this->now(),
557 'meta' => $item->meta,
558 ],
559 ['%d', '%s', '%s', '%d', '%d', '%d', '%s', '%s', '%s']
560 );
561 }
562 }
563
564 // Transfer guest lists ownership for Pro multi-list support.
565 $wpdb->update(
566 $lists_table,
567 [
568 'user_id' => $user_id,
569 'session_key' => '',
570 'updated_at' => $this->now(),
571 ],
572 [
573 'session_key' => $session_key,
574 ],
575 ['%d', '%s', '%s'],
576 ['%s']
577 );
578
579 // Remove guest rows to avoid duplication.
580 $wpdb->delete(
581 $items_table,
582 ['session_key' => $session_key],
583 ['%s']
584 );
585
586 $this->session->clear();
587 $this->invalidate_cache(self::DEFAULT_WISHLIST_ID);
588 }
589
590 /**
591 * Remove cached counts for the wishlist.
592 *
593 * @param string $wishlist_id Wishlist identifier.
594 * @return void
595 */
596 public function invalidate_cache(string $wishlist_id): void
597 {
598 delete_transient($this->get_cache_key($wishlist_id));
599 }
600
601 /**
602 * Ensure a default list row exists.
603 *
604 * @param string $wishlist_id Wishlist identifier.
605 * @return void
606 */
607 public function ensure_default_list(string $wishlist_id): void
608 {
609 global $wpdb;
610
611 $lists_table = Wishlist_DB::get_lists_table();
612
613 $existing_row = $wpdb->get_var(
614 $wpdb->prepare(
615 "SELECT id FROM {$lists_table} WHERE (user_id = %d OR session_key = %s) AND slug = %s LIMIT 1",
616 $this->user_id,
617 $this->session_key,
618 $wishlist_id
619 )
620 );
621
622 if ($existing_row) {
623 return;
624 }
625
626 $wpdb->insert(
627 $lists_table,
628 [
629 'user_id' => $this->user_id,
630 'session_key' => $this->user_id > 0 ? '' : $this->session_key,
631 'title' => esc_html__('My Wishlist', 'king-addons'),
632 'slug' => $wishlist_id,
633 'visibility' => 'private',
634 'created_at' => $this->now(),
635 'updated_at' => $this->now(),
636 ],
637 ['%d', '%s', '%s', '%s', '%s', '%s', '%s']
638 );
639 }
640
641 /**
642 * Get a single wishlist item row.
643 *
644 * @param string $wishlist_id Wishlist identifier.
645 * @param int $product_id Product identifier.
646 * @param int $variation_id Variation identifier.
647 * @return object|null Wishlist row.
648 */
649 private function get_item_row(string $wishlist_id, int $product_id, int $variation_id): ?object
650 {
651 global $wpdb;
652 $table = Wishlist_DB::get_items_table();
653 $where = $this->get_scope_where($wishlist_id);
654 $where['sql'] .= ' AND product_id = %d AND variation_id = %d';
655 $where['params'][] = $product_id;
656 $where['params'][] = $variation_id;
657
658 $query = "SELECT * FROM {$table} WHERE {$where['sql']} LIMIT 1";
659
660 return $wpdb->get_row($wpdb->prepare($query, $where['params']));
661 }
662
663 /**
664 * Validate product and variation existence.
665 *
666 * @param int $product_id Product identifier.
667 * @param int $variation_id Variation identifier.
668 * @return bool|WP_Error Validation result.
669 */
670 private function validate_product(int $product_id, int $variation_id)
671 {
672 if (!function_exists('wc_get_product')) {
673 return new WP_Error('wishlist_no_wc', esc_html__('WooCommerce is required for wishlist.', 'king-addons'));
674 }
675
676 $product = wc_get_product($variation_id > 0 ? $variation_id : $product_id);
677 if (!$product) {
678 return new WP_Error('wishlist_invalid_product', esc_html__('Product not found.', 'king-addons'));
679 }
680
681 return true;
682 }
683
684 /**
685 * Build scope-aware WHERE clause for queries.
686 *
687 * @param string $wishlist_id Wishlist identifier.
688 * @return array{sql:string,params:array<int, mixed>} Query fragment.
689 */
690 private function get_scope_where(string $wishlist_id): array
691 {
692 if ($this->user_id > 0) {
693 return [
694 'sql' => 'user_id = %d AND wishlist_id = %s',
695 'params' => [$this->user_id, $wishlist_id],
696 ];
697 }
698
699 return [
700 'sql' => 'session_key = %s AND wishlist_id = %s',
701 'params' => [$this->session_key, $wishlist_id],
702 ];
703 }
704
705 /**
706 * Normalize wishlist identifier.
707 *
708 * @param string|null $wishlist_id Wishlist identifier.
709 * @return string Normalized wishlist id.
710 */
711 private function normalize_wishlist_id(?string $wishlist_id): string
712 {
713 $resolved = $wishlist_id ?: $this->active_wishlist_id ?: self::DEFAULT_WISHLIST_ID;
714 $resolved = sanitize_title($resolved);
715
716 if (empty($resolved)) {
717 $resolved = self::DEFAULT_WISHLIST_ID;
718 }
719
720 return $resolved;
721 }
722
723 /**
724 * Resolve the active wishlist id from user meta or default.
725 *
726 * @return string Active wishlist id.
727 */
728 private function resolve_active_wishlist_id(): string
729 {
730 if ($this->user_id > 0) {
731 $saved = get_user_meta($this->user_id, 'king_addons_active_wishlist_id', true);
732 if (!empty($saved)) {
733 return $this->normalize_wishlist_id($saved);
734 }
735 }
736
737 return self::DEFAULT_WISHLIST_ID;
738 }
739
740 /**
741 * Build cache key per scope and wishlist.
742 *
743 * @param string $wishlist_id Wishlist identifier.
744 * @return string Cache key.
745 */
746 private function get_cache_key(string $wishlist_id): string
747 {
748 $scope = $this->user_id > 0 ? 'user-' . $this->user_id : 'sess-' . $this->session_key;
749 return 'king_addons_wishlist_count_' . $scope . '_' . $wishlist_id;
750 }
751
752 /**
753 * Current UTC datetime string for DB writes.
754 *
755 * @return string Datetime in mysql format.
756 */
757 private function now(): string
758 {
759 $dt = new DateTime('now', new DateTimeZone('UTC'));
760 return $dt->format('Y-m-d H:i:s');
761 }
762 }
763
764
765
766