PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.12.4
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.12.4
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / includes / core / rest-api / Controllers / DashboardController.php

DashboardController.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.12.4, at includes/core/rest-api/Controllers/DashboardController.php

549 lines 14.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 namespace WPFunnels\Rest\Controllers;
5
6 use WPFunnels\Report\ReportGenerator;
7 use WPFunnels\Wpfnl_functions;
8
9 /**
10 *
11 *
12 * Class DashboardController
13 * @package WPFunnels\Rest\Controllers
14 * @since 3.2.0
15 */
16 class DashboardController extends Wpfnl_REST_Controller
17 {
18
19 /**
20 * Endpoint namespace.
21 *
22 * @var string
23 * @since 3.2.0
24 */
25 protected $namespace = 'wpfunnels/v1';
26
27
28 /**
29 * Route base.
30 *
31 * @var string
32 * @since 3.2.0
33 */
34 protected $rest_base = 'report';
35
36
37 /**
38 * Makes sure the current user has access to READ the settings APIs.
39 *
40 *
41 * @param $request
42 * @return \WP_Error|boolean
43 * @since 3.2.0
44 */
45 public function get_items_permissions_check($request)
46 {
47 if (!Wpfnl_functions::wpfnl_rest_check_manager_permissions('settings')) {
48 return new \WP_Error('wpfunnels_rest_cannot_edit', __('Sorry, you cannot list resources.', 'wpfnl'), array('status' => rest_authorization_required_code()));
49 }
50 return true;
51 }
52
53
54 /**
55 * Register rest routes
56 *
57 * @since 3.2.0
58 */
59 public function register_routes()
60 {
61 register_rest_route(
62 $this->namespace,
63 '/' . $this->rest_base . '/overview',
64 array(
65 array(
66 'methods' => \WP_REST_Server::READABLE,
67 'args' => $this->get_stats_args(),
68 'callback' => array($this, 'get_overview'),
69 'permission_callback' => array($this, 'get_items_permissions_check'),
70 ),
71 )
72 );
73
74 register_rest_route(
75 $this->namespace,
76 '/' . $this->rest_base . '/stats',
77 array(
78 array(
79 'methods' => \WP_REST_Server::READABLE,
80 'args' => $this->get_stats_args(),
81 'callback' => array($this, 'get_stats'),
82 'permission_callback' => array($this, 'get_items_permissions_check'),
83 ),
84 )
85 );
86
87 register_rest_route(
88 $this->namespace,
89 '/' . $this->rest_base . '/top-funnels',
90 array(
91 array(
92 'methods' => \WP_REST_Server::READABLE,
93 'args' => $this->get_stats_args(),
94 'callback' => array($this, 'get_top_funnels'),
95 'permission_callback' => array($this, 'get_items_permissions_check'),
96 ),
97 )
98 );
99
100 register_rest_route(
101 $this->namespace,
102 '/' . $this->rest_base . '/onboarding-status',
103 array(
104 array(
105 'methods' => \WP_REST_Server::READABLE,
106 'callback' => array($this, 'get_onboarding_status'),
107 'permission_callback' => array($this, 'get_items_permissions_check'),
108 ),
109 )
110 );
111
112 register_rest_route(
113 $this->namespace,
114 '/' . $this->rest_base . '/has-funnels',
115 array(
116 array(
117 'methods' => \WP_REST_Server::READABLE,
118 'callback' => array($this, 'check_has_funnels'),
119 'permission_callback' => array( $this, 'get_items_permissions_check' ),
120 ),
121 )
122 );
123
124 register_rest_route(
125 $this->namespace,
126 '/' . $this->rest_base . '/dismiss-onboarding',
127 array(
128 array(
129 'methods' => \WP_REST_Server::CREATABLE,
130 'callback' => array($this, 'dismiss_onboarding'),
131 'permission_callback' => array( $this, 'get_items_permissions_check' ),
132 ),
133 )
134 );
135
136 register_rest_route(
137 $this->namespace,
138 '/' . $this->rest_base . '/sync-cache',
139 array(
140 array(
141 'methods' => \WP_REST_Server::CREATABLE,
142 'callback' => array( $this, 'sync_cache' ),
143 'permission_callback' => array( $this, 'get_items_permissions_check' ),
144 ),
145 )
146 );
147 }
148
149 /**
150 * Get stats arguments
151 *
152 * @return mixed|void
153 * @since 3.2.0
154 */
155 public function get_stats_args()
156 {
157 return array(
158 'after' => array(
159 'type' => 'string',
160 'format' => 'date-time',
161 'validate_callback' => 'rest_validate_request_arg',
162 ),
163 'before' => array(
164 'type' => 'string',
165 'format' => 'date-time',
166 'validate_callback' => 'rest_validate_request_arg',
167 ),
168 'interval' => array(
169 'type' => 'string',
170 'default' => 'week',
171 'enum' => array(
172 'hour',
173 'day',
174 'week',
175 'month',
176 'quarter',
177 'year',
178 ),
179 ),
180 );
181 }
182
183
184 /**
185 * Build a cache key from a prefix and parameter array.
186 *
187 * @param string $prefix
188 * @param array $params
189 * @return string
190 *
191 * @since 3.9.6
192 */
193 protected function get_cache_key( $prefix, $params ) {
194 return 'wpfnl_dash_' . $prefix . '_' . md5( serialize( $params ) );
195 }
196
197
198 /**
199 * Delete all dashboard transients from the database.
200 * Called by the sync-cache endpoint or when cache must be invalidated.
201 *
202 * @since 3.9.6
203 */
204 public static function delete_dashboard_cache() {
205 global $wpdb;
206 $wpdb->query(
207 "DELETE FROM {$wpdb->options}
208 WHERE option_name LIKE '_transient_wpfnl_dash_%'
209 OR option_name LIKE '_transient_timeout_wpfnl_dash_%'"
210 );
211 }
212
213
214 /**
215 * Get overview data of funnels (with 24-hour server-side cache).
216 *
217 * @param \WP_REST_Request $request
218 * @return \WP_Error|\WP_REST_Response
219 * @throws \Exception
220 *
221 * @since 3.2.0
222 */
223 public function get_overview(\WP_REST_Request $request)
224 {
225 $params = $request->get_params();
226 $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s');
227 $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s');
228
229 $cache_key = $this->get_cache_key( 'ov', array( $start_date, $end_date ) );
230 $cached = get_transient( $cache_key );
231 if ( false !== $cached ) {
232 return rest_ensure_response( $cached );
233 }
234
235 $response = ReportGenerator::get_overview( $start_date, $end_date );
236 set_transient( $cache_key, $response, DAY_IN_SECONDS );
237
238 return rest_ensure_response( $response );
239 }
240
241
242 /**
243 * Get stats of the funnels with intervals period (with 24-hour server-side cache).
244 *
245 * @param \WP_REST_Request $request
246 * @return \WP_Error|\WP_REST_Response
247 * @throws \Exception
248 *
249 * @since 3.2.0
250 */
251 public function get_stats(\WP_REST_Request $request)
252 {
253 $params = $request->get_params();
254 $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s');
255 $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s');
256 $interval = isset($params['interval']) ? $params['interval'] : 'day';
257
258 $cache_key = $this->get_cache_key( 'st', array( $start_date, $end_date, $interval ) );
259 $cached = get_transient( $cache_key );
260 if ( false !== $cached ) {
261 return rest_ensure_response( $cached );
262 }
263
264 $response = ReportGenerator::get_stats( $start_date, $end_date, $interval );
265 set_transient( $cache_key, $response, DAY_IN_SECONDS );
266
267 return rest_ensure_response( $response );
268 }
269
270
271 /**
272 * Get 3 top performing funnels
273 *
274 * @param \WP_REST_Request $request
275 * @return \WP_Error|\WP_REST_Response
276 *
277 * @since 3.2.0
278 */
279 public function get_top_funnels(\WP_REST_Request $request)
280 {
281 $params = $request->get_params();
282 $start_date = isset( $params['after'] ) ? $params['after'] : $this->default_after()->format( 'Y-m-d H:i:s' );
283 $end_date = isset( $params['before'] ) ? $params['before'] : $this->default_before()->format( 'Y-m-d H:i:s' );
284
285 $response['status'] = true;
286 $response['data'] = ReportGenerator::get_top_funnels( $start_date, $end_date );
287 return rest_ensure_response( $response );
288 }
289
290
291 /**
292 * Get onboarding status.
293 *
294 * Determines whether the onboarding section should be shown on the dashboard.
295 * The onboarding is hidden when ALL three conditions are met:
296 * 1. At least one funnel is published (live)
297 * 2. At least one funnel contains an order bump (upsell is optional/bonus)
298 * 3. At least one real order has been placed through any funnel
299 *
300 * Also returns individual step completion states for the checklist.
301 *
302 * @return \WP_REST_Response
303 * @since 3.9.5
304 */
305 public function get_onboarding_status()
306 {
307 // ── Check if onboarding has been dismissed ──
308 $is_dismissed = get_transient( 'wpfnl_onboarding_dismissed' );
309 if ( $is_dismissed ) {
310 return rest_ensure_response( array(
311 'success' => true,
312 'show_onboarding' => false,
313 'steps' => array(),
314 'completed_count' => 0,
315 'total_steps' => 0,
316 'progress_percent' => 0,
317 ) );
318 }
319
320 $is_wc_active = class_exists('WooCommerce');
321 $has_live_funnel = false;
322 $has_order_bump = false;
323 $has_upsell = false;
324 $has_real_order = false;
325 $has_funnel_created = false;
326 $has_store_checkout_created = false;
327
328 // Meta query to exclude store checkout funnels
329 $exclude_sc_meta = array(
330 'relation' => 'OR',
331 array(
332 'key' => '_wpfnl_funnel_type',
333 'compare' => 'NOT EXISTS',
334 ),
335 array(
336 'key' => '_wpfnl_funnel_type',
337 'value' => 'store_checkout',
338 'compare' => '!=',
339 ),
340 );
341
342 // ── Step 1: Check for published funnels (excluding store checkouts) ──
343 $funnels = get_posts(array(
344 'post_type' => WPFNL_FUNNELS_POST_TYPE,
345 'post_status' => 'publish',
346 'numberposts' => -1,
347 'fields' => 'ids',
348 'meta_query' => $exclude_sc_meta,
349 ));
350
351 if (!empty($funnels)) {
352 $has_funnel_created = true;
353 $has_live_funnel = true;
354 }
355
356 // Also check for draft funnels (counts as "created" but not "live")
357 if (!$has_funnel_created) {
358 $draft_funnels = get_posts(array(
359 'post_type' => WPFNL_FUNNELS_POST_TYPE,
360 'post_status' => array('publish', 'draft'),
361 'numberposts' => 1,
362 'fields' => 'ids',
363 'meta_query' => $exclude_sc_meta,
364 ));
365 $has_funnel_created = !empty($draft_funnels);
366 }
367
368 // ── Check for store checkout funnels separately ──
369 $sc_funnels = get_posts(array(
370 'post_type' => WPFNL_FUNNELS_POST_TYPE,
371 'post_status' => array('publish', 'draft'),
372 'numberposts' => 1,
373 'fields' => 'ids',
374 'meta_query' => array(
375 array(
376 'key' => '_wpfnl_funnel_type',
377 'value' => 'store_checkout',
378 ),
379 ),
380 ));
381 $has_store_checkout_created = !empty($sc_funnels);
382
383 // ── Step 2: Check for order bump & upsell across all published funnels ──
384 if (!empty($funnels)) {
385 foreach ($funnels as $funnel_id) {
386 $steps = Wpfnl_functions::get_steps($funnel_id);
387
388 if (!is_array($steps) || empty($steps)) {
389 continue;
390 }
391
392 foreach ($steps as $step) {
393 $step_id = isset($step['id']) ? $step['id'] : 0;
394 $step_type = isset($step['step_type']) ? $step['step_type'] : '';
395
396 // Check for order bump on checkout steps
397 if ('checkout' === $step_type && !$has_order_bump) {
398 $ob_settings = get_post_meta($step_id, 'order-bump-settings', true);
399 if (!empty($ob_settings) && is_array($ob_settings)) {
400 $is_multidimensional = Wpfnl_functions::check_array_is_multidimentional($ob_settings);
401 if ($is_multidimensional) {
402 foreach ($ob_settings as $ob) {
403 if (!empty($ob['product']) && !empty($ob['isEnabled'])) {
404 $has_order_bump = true;
405 break;
406 }
407 }
408 }
409 else {
410 // Single OB (legacy format)
411 if (!empty($ob_settings['product'])) {
412 $has_order_bump = true;
413 }
414 }
415 }
416 }
417
418 // Check for upsell step
419 if ('upsell' === $step_type) {
420 $has_upsell = true;
421 }
422 }
423
424 // If we already found both, no need to check more funnels
425 if ($has_order_bump && $has_upsell) {
426 break;
427 }
428 }
429 }
430
431 // ── Step 3: Check for real orders through funnels ──
432 if ($is_wc_active && !empty($funnels)) {
433 global $wpdb;
434 $stats_table = $wpdb->prefix . 'wpfnl_stats';
435
436 // Check if stats table exists
437 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $stats_table));
438 if ($table_exists) {
439 $order_count = $wpdb->get_var(
440 "SELECT COUNT(id) FROM {$stats_table} WHERE status IN ('completed', 'processing') LIMIT 1"
441 );
442 $has_real_order = (int)$order_count > 0;
443 }
444 }
445
446 // ── Determine if onboarding should be shown ──
447 // Hide onboarding only when ALL conditions are met:
448 // - At least 1 live funnel
449 // - Has an order bump (upsell is optional)
450 // - Has at least 1 real order
451 $should_hide_onboarding = $has_live_funnel && $has_order_bump && $has_real_order;
452
453 // ── Build checklist step completion data ──
454 $completed_count = 0;
455 $steps_status = array(
456 'woocommerce_connected' => $is_wc_active,
457 'funnel_created' => $has_funnel_created,
458 'store_checkout_created' => $has_store_checkout_created,
459 'order_bump_added' => $has_order_bump,
460 'upsell_added' => $has_upsell,
461 'test_order_placed' => $has_real_order,
462 'funnel_live' => $has_live_funnel,
463 );
464
465 foreach ($steps_status as $status) {
466 if ($status) {
467 $completed_count++;
468 }
469 }
470
471 $total_steps = count($steps_status);
472 $progress_percent = $total_steps > 0 ? round(($completed_count / $total_steps) * 100) : 0;
473
474 return rest_ensure_response(array(
475 'success' => true,
476 'show_onboarding' => !$should_hide_onboarding,
477 'steps' => $steps_status,
478 'completed_count' => $completed_count,
479 'total_steps' => $total_steps,
480 'progress_percent' => $progress_percent,
481 ));
482 }
483
484 /**
485 * Check if there are any funnels
486 *
487 * @param \WP_REST_Request $request
488 * @return \WP_REST_Response
489 *
490 * @since 3.9.5
491 */
492 public function check_has_funnels( \WP_REST_Request $request ) {
493 global $wpdb;
494
495 // Count only non-store-checkout funnels
496 $funnel_count = $wpdb->get_var(
497 $wpdb->prepare(
498 "SELECT COUNT(*) FROM {$wpdb->posts} p
499 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wpfnl_funnel_type'
500 WHERE p.post_type = %s
501 AND p.post_status IN ('publish', 'draft')
502 AND (pm.meta_value IS NULL OR pm.meta_value != 'store_checkout')",
503 'wpfunnels'
504 )
505 );
506
507 return rest_ensure_response( array(
508 'success' => true,
509 'has_funnels' => (int) $funnel_count > 0,
510 'funnel_count' => (int) $funnel_count
511 ) );
512 }
513
514 /**
515 * Clear all dashboard transient caches so the next request fetches fresh data.
516 *
517 * @param \WP_REST_Request $request
518 * @return \WP_REST_Response
519 *
520 * @since 3.9.6
521 */
522 public function sync_cache( \WP_REST_Request $request ) {
523 self::delete_dashboard_cache();
524 return rest_ensure_response( array(
525 'success' => true,
526 'message' => __( 'Dashboard cache cleared. Data will refresh on next load.', 'wpfnl' ),
527 ) );
528 }
529
530
531 /**
532 * Dismiss the onboarding checklist
533 *
534 * @param \WP_REST_Request $request
535 * @return \WP_REST_Response
536 *
537 * @since 3.9.5
538 */
539 public function dismiss_onboarding( \WP_REST_Request $request ) {
540 // Store dismissal in a transient for 30 days
541 set_transient( 'wpfnl_onboarding_dismissed', true, 30 * DAY_IN_SECONDS );
542
543 return rest_ensure_response( array(
544 'success' => true,
545 'message' => 'Onboarding dismissed successfully'
546 ) );
547 }
548 }
549