PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.1
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.1
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 2.8.17 2.8.18 All 259 releases
wpfunnels / includes / core / MCP / Tools / ReportTools.php

ReportTools.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.1, at includes/core/MCP/Tools/ReportTools.php

506 lines 17.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ReportTools — MCP abilities for WPFunnels Free "Reports" domain (plan §5.1).
4 *
5 * Replaces the old `AnalyticsTools.php`, which mixed this Free-domain
6 * `get-dashboard-overview` tool with two Pro-domain analytics tools
7 * (`get-funnel-analytics`, `get-offer-performance` — now moved to
8 * `wpfunnels-pro/includes/core/mcp/Tools/AnalyticsTools.php`).
9 *
10 * `get-dashboard-overview` is rewired to the real `ReportGenerator::get_overview()`
11 * (the same class `DashboardController`'s `/report/overview` REST route calls)
12 * instead of the previous hardcoded numbers, which also called a
13 * `MCPHelper::countFunnels()` method that does not exist anywhere in the
14 * codebase and would have fataled on first use.
15 *
16 * `list-funnel-orders` and `get-recent-logs` are the plan's other two Free
17 * "Reports" tools, backed respectively by the real `{prefix}wpfnl_stats`
18 * parent/child order table (see `includes/utils/class-wpfnl-activator.php`)
19 * and the real `Wpfnl_Logger` log file registry.
20 *
21 * @package WPFunnels\MCP\Tools
22 * @since 3.13.0
23 */
24
25 namespace WPFunnels\MCP\Tools;
26
27 defined( 'ABSPATH' ) || exit;
28
29 use WPFunnels\MCP\Helpers\MCPHelper;
30
31 /**
32 * Class ReportTools
33 */
34 class ReportTools {
35
36 /**
37 * Register tool definitions.
38 *
39 * @return array
40 */
41 public static function definitions() {
42 return [
43 'wpfunnels/get-dashboard-overview' => [
44 'description' => 'Get overall store and funnel performance metrics (total revenue, orders, conversion rate, active funnels).',
45 'category' => 'reports',
46 'readonly' => true,
47 'destructive' => false,
48 'parameters' => [
49 'type' => 'object',
50 'properties' => [
51 'period' => [
52 'type' => 'string',
53 'enum' => [ '7days', '30days', 'this_month', 'all_time' ],
54 'description' => 'Time period for metrics.',
55 ],
56 ],
57 ],
58 'callback' => [ __CLASS__, 'getDashboardOverview' ],
59 ],
60
61 'wpfunnels/list-funnel-orders' => [
62 'description' => 'List parent orders and their child offer orders (order bump / upsell / downsell) placed through funnels, with a per-order revenue breakdown.',
63 'category' => 'reports',
64 'readonly' => true,
65 'destructive' => false,
66 'parameters' => [
67 'type' => 'object',
68 'properties' => array_merge(
69 [
70 'funnel_id' => [
71 'type' => 'integer',
72 'description' => 'Filter by funnel ID (0 or omitted for all funnels).',
73 ],
74 ],
75 MCPHelper::paginationSchema()
76 ),
77 ],
78 'callback' => [ __CLASS__, 'listFunnelOrders' ],
79 ],
80
81 'wpfunnels/get-recent-logs' => [
82 'description' => 'Read the most recent lines from the WPFunnels debug/error log files, for diagnosing a reported problem.',
83 'category' => 'reports',
84 'readonly' => true,
85 'destructive' => false,
86 'parameters' => [
87 'type' => 'object',
88 'properties' => [
89 'lines' => [
90 'type' => 'integer',
91 'description' => 'Max lines to return per log file (max 200).',
92 'minimum' => 1,
93 'maximum' => 200,
94 'default' => 50,
95 ],
96 ],
97 ],
98 'callback' => [ __CLASS__, 'getRecentLogs' ],
99 ],
100
101 'wpfunnels/get-report-stats' => [
102 'description' => 'Get interval-bucketed dashboard chart data (orders, customers, checkout/order-bump/funnel/store sales, leads) between two dates — the same data backing the dashboard charts. Defaults to the last year, bucketed weekly, when no dates are given.',
103 'category' => 'reports',
104 'readonly' => true,
105 'destructive' => false,
106 'parameters' => [
107 'type' => 'object',
108 'properties' => [
109 'after' => [
110 'type' => 'string',
111 'description' => 'Start of the date range, e.g. "2026-01-01 00:00:00". Defaults to one year ago.',
112 ],
113 'before' => [
114 'type' => 'string',
115 'description' => 'End of the date range, e.g. "2026-08-25 23:59:59". Defaults to now.',
116 ],
117 'interval' => [
118 'type' => 'string',
119 'enum' => [ 'hour', 'day', 'week', 'month', 'quarter', 'year' ],
120 'description' => 'Bucket size for the interval breakdown.',
121 'default' => 'week',
122 ],
123 ],
124 ],
125 'callback' => [ __CLASS__, 'getReportStats' ],
126 ],
127
128 'wpfunnels/get-top-funnels' => [
129 'description' => 'Get the top 3 performing funnels by completed-order revenue (orderbump + upsell + downsell + base sales) between two dates, each with order count, revenue, AOV, and AOV lift vs. the store average.',
130 'category' => 'reports',
131 'readonly' => true,
132 'destructive' => false,
133 'parameters' => [
134 'type' => 'object',
135 'properties' => [
136 'after' => [
137 'type' => 'string',
138 'description' => 'Start of the date range, e.g. "2026-01-01 00:00:00". Defaults to one year ago.',
139 ],
140 'before' => [
141 'type' => 'string',
142 'description' => 'End of the date range, e.g. "2026-08-25 23:59:59". Defaults to now.',
143 ],
144 ],
145 ],
146 'callback' => [ __CLASS__, 'getTopFunnels' ],
147 ],
148
149 'wpfunnels/get-onboarding-status' => [
150 'description' => 'Get the dashboard onboarding checklist state: whether it should still be shown, per-step completion (WooCommerce connected, funnel created, store checkout created, order bump added, upsell added, test order placed, funnel live), and overall progress.',
151 'category' => 'reports',
152 'readonly' => true,
153 'destructive' => false,
154 'parameters' => [
155 'type' => 'object',
156 'properties' => [],
157 ],
158 'callback' => [ __CLASS__, 'getOnboardingStatus' ],
159 ],
160
161 'wpfunnels/dismiss-onboarding' => [
162 'description' => 'Dismiss the dashboard onboarding checklist for 30 days. This only sets a UI flag (a transient) — it does not change or delete any funnel, order, or setting, and reverses itself automatically after 30 days.',
163 'category' => 'reports',
164 'readonly' => false,
165 'destructive' => false,
166 'parameters' => [
167 'type' => 'object',
168 'properties' => [],
169 ],
170 'callback' => [ __CLASS__, 'dismissOnboarding' ],
171 ],
172
173 'wpfunnels/sync-report-cache' => [
174 'description' => 'Force the dashboard report cache to refresh by clearing its cached transients (overview/stats/top-funnels results cached for 24 hours). Only deletes derived cache entries, not real orders/funnels/settings — the next dashboard read simply recomputes and re-caches them, at the cost of a slightly slower next load.',
175 'category' => 'reports',
176 'readonly' => false,
177 'destructive' => false,
178 'parameters' => [
179 'type' => 'object',
180 'properties' => [],
181 ],
182 'callback' => [ __CLASS__, 'syncReportCache' ],
183 ],
184 ];
185 }
186
187 /**
188 * Resolve a period slug into a [start_date, end_date] pair, matching the
189 * date-time format `ReportGenerator` expects.
190 *
191 * @param string $period Period slug.
192 * @return array{0:string,1:string}
193 */
194 private static function periodRange( $period ) {
195 $now = current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
196
197 switch ( $period ) {
198 case '7days':
199 $start = gmdate( 'Y-m-d H:i:s', strtotime( '-7 days', $now ) );
200 break;
201 case 'this_month':
202 $start = gmdate( 'Y-m-01 00:00:00', $now );
203 break;
204 case 'all_time':
205 $start = '2000-01-01 00:00:00';
206 break;
207 case '30days':
208 default:
209 $start = gmdate( 'Y-m-d H:i:s', strtotime( '-30 days', $now ) );
210 break;
211 }
212
213 return [ $start, gmdate( 'Y-m-d H:i:s', $now ) ];
214 }
215
216 /**
217 * Count funnels in a given post status.
218 *
219 * @param string $status Post status.
220 * @return int
221 */
222 private static function countFunnels( $status = 'publish' ) {
223 $post_type = defined( 'WPFNL_FUNNELS_POST_TYPE' ) ? WPFNL_FUNNELS_POST_TYPE : 'wpfunnels';
224 $counts = wp_count_posts( $post_type );
225
226 return isset( $counts->{$status} ) ? (int) $counts->{$status} : 0;
227 }
228
229 /**
230 * Get dashboard overview, via the real ReportGenerator used by
231 * DashboardController's `/report/overview` REST route.
232 *
233 * @param array $input Tool input.
234 * @return array|\WP_Error
235 */
236 public static function getDashboardOverview( $input = [] ) {
237 $period = isset( $input['period'] ) ? sanitize_text_field( $input['period'] ) : '30days';
238 if ( ! in_array( $period, [ '7days', '30days', 'this_month', 'all_time' ], true ) ) {
239 $period = '30days';
240 }
241
242 if ( ! class_exists( '\WPFunnels\Report\ReportGenerator' ) ) {
243 return MCPHelper::error( 'report_unavailable', 'The WPFunnels reporting module is not available on this install.' );
244 }
245
246 list( $start_date, $end_date ) = self::periodRange( $period );
247
248 $overview = \WPFunnels\Report\ReportGenerator::get_overview( $start_date, $end_date );
249 $data = isset( $overview['data'] ) && is_array( $overview['data'] ) ? $overview['data'] : [];
250
251 $data['period'] = $period;
252 $data['active_funnels'] = self::countFunnels( 'publish' );
253
254 return $data;
255 }
256
257 /**
258 * Get an instance of the real DashboardController, whose non-ReportGenerator
259 * logic (onboarding checklist, cache sync) lives only on the controller
260 * itself, with no separate service class to call statically.
261 *
262 * @return \WPFunnels\Rest\Controllers\DashboardController|null
263 */
264 private static function dashboardController() {
265 if ( ! class_exists( '\WPFunnels\Rest\Controllers\DashboardController' ) ) {
266 return null;
267 }
268
269 return new \WPFunnels\Rest\Controllers\DashboardController();
270 }
271
272 /**
273 * Unwrap a WP_REST_Response into its plain data array, matching how
274 * getDashboardOverview() already treats ReportGenerator's array returns.
275 *
276 * @param mixed $response Controller return value.
277 * @return mixed
278 */
279 private static function unwrapResponse( $response ) {
280 return $response instanceof \WP_REST_Response ? $response->get_data() : $response;
281 }
282
283 /**
284 * Resolve the after/before date-range params shared by get_stats() and
285 * get_top_funnels(), matching DashboardController::default_after()/
286 * default_before() (1 year ago .. now, in the site's timezone) when omitted.
287 *
288 * @param array $input Tool input.
289 * @param \WPFunnels\Rest\Controllers\DashboardController $controller Controller instance.
290 * @return array{0:string,1:string}
291 */
292 private static function statsDateRange( $input, $controller ) {
293 $after = isset( $input['after'] ) && '' !== $input['after']
294 ? sanitize_text_field( $input['after'] )
295 : $controller->default_after()->format( 'Y-m-d H:i:s' );
296
297 $before = isset( $input['before'] ) && '' !== $input['before']
298 ? sanitize_text_field( $input['before'] )
299 : $controller->default_before()->format( 'Y-m-d H:i:s' );
300
301 return [ $after, $before ];
302 }
303
304 /**
305 * Get interval-bucketed stats, via the real ReportGenerator::get_stats()
306 * used by DashboardController's `/report/stats` REST route (called
307 * directly, bypassing that route's 24-hour transient cache, for freshness —
308 * same rationale as getDashboardOverview()).
309 *
310 * @param array $input Tool input.
311 * @return array|\WP_Error
312 */
313 public static function getReportStats( $input = [] ) {
314 if ( ! class_exists( '\WPFunnels\Report\ReportGenerator' ) ) {
315 return MCPHelper::error( 'report_unavailable', 'The WPFunnels reporting module is not available on this install.' );
316 }
317
318 $controller = self::dashboardController();
319 if ( ! $controller ) {
320 return MCPHelper::error( 'report_unavailable', 'The WPFunnels dashboard controller is not available on this install.' );
321 }
322
323 list( $after, $before ) = self::statsDateRange( $input, $controller );
324
325 $interval = isset( $input['interval'] ) ? sanitize_text_field( $input['interval'] ) : 'week';
326 if ( ! in_array( $interval, [ 'hour', 'day', 'week', 'month', 'quarter', 'year' ], true ) ) {
327 $interval = 'week';
328 }
329
330 return \WPFunnels\Report\ReportGenerator::get_stats( $after, $before, $interval );
331 }
332
333 /**
334 * Get the top 3 performing funnels, via the real
335 * ReportGenerator::get_top_funnels() used by DashboardController's
336 * `/report/top-funnels` REST route.
337 *
338 * @param array $input Tool input.
339 * @return array|\WP_Error
340 */
341 public static function getTopFunnels( $input = [] ) {
342 if ( ! class_exists( '\WPFunnels\Report\ReportGenerator' ) ) {
343 return MCPHelper::error( 'report_unavailable', 'The WPFunnels reporting module is not available on this install.' );
344 }
345
346 $controller = self::dashboardController();
347 if ( ! $controller ) {
348 return MCPHelper::error( 'report_unavailable', 'The WPFunnels dashboard controller is not available on this install.' );
349 }
350
351 list( $after, $before ) = self::statsDateRange( $input, $controller );
352
353 return [
354 'status' => true,
355 'data' => \WPFunnels\Report\ReportGenerator::get_top_funnels( $after, $before ),
356 ];
357 }
358
359 /**
360 * Get onboarding checklist status, via the real
361 * DashboardController::get_onboarding_status().
362 *
363 * @param array $input Tool input (unused — no params).
364 * @return array|\WP_Error
365 */
366 public static function getOnboardingStatus( $input = [] ) {
367 $controller = self::dashboardController();
368 if ( ! $controller ) {
369 return MCPHelper::error( 'report_unavailable', 'The WPFunnels dashboard controller is not available on this install.' );
370 }
371
372 return self::unwrapResponse( $controller->get_onboarding_status() );
373 }
374
375 /**
376 * Dismiss the onboarding checklist for 30 days, via the real
377 * DashboardController::dismiss_onboarding().
378 *
379 * @param array $input Tool input (unused — no params).
380 * @return array|\WP_Error
381 */
382 public static function dismissOnboarding( $input = [] ) {
383 $controller = self::dashboardController();
384 if ( ! $controller ) {
385 return MCPHelper::error( 'report_unavailable', 'The WPFunnels dashboard controller is not available on this install.' );
386 }
387
388 return self::unwrapResponse( $controller->dismiss_onboarding( new \WP_REST_Request() ) );
389 }
390
391 /**
392 * Force the dashboard report cache to refresh, via the real
393 * DashboardController::sync_cache(). This only deletes the
394 * `wpfnl_dash_*` transients (see DashboardController::delete_dashboard_cache()) —
395 * cheap, idempotent, and self-healing, since the next overview/stats/
396 * top-funnels read simply recomputes and re-caches them. It touches no
397 * funnel, order, or settings data, so it is not marked destructive.
398 *
399 * @param array $input Tool input (unused — no params).
400 * @return array|\WP_Error
401 */
402 public static function syncReportCache( $input = [] ) {
403 $controller = self::dashboardController();
404 if ( ! $controller ) {
405 return MCPHelper::error( 'report_unavailable', 'The WPFunnels dashboard controller is not available on this install.' );
406 }
407
408 return self::unwrapResponse( $controller->sync_cache( new \WP_REST_Request() ) );
409 }
410
411 /**
412 * List parent/child funnel orders from the real `{prefix}wpfnl_stats`
413 * table (see includes/utils/class-wpfnl-activator.php for the schema).
414 *
415 * @param array $input Tool input.
416 * @return array
417 */
418 public static function listFunnelOrders( $input = [] ) {
419 global $wpdb;
420
421 $funnel_id = isset( $input['funnel_id'] ) ? (int) $input['funnel_id'] : 0;
422 $page = isset( $input['page'] ) ? max( 1, (int) $input['page'] ) : 1;
423 $per_page = MCPHelper::perPage( isset( $input['per_page'] ) ? $input['per_page'] : MCPHelper::DEFAULT_PER_PAGE );
424
425 $table = $wpdb->prefix . 'wpfnl_stats';
426 $where = $funnel_id ? $wpdb->prepare( 'WHERE funnel_id = %d', $funnel_id ) : '';
427
428 $total = (int) $wpdb->get_var( "SELECT COUNT(id) FROM {$table} {$where}" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
429
430 $offset = ( $page - 1 ) * $per_page;
431 $rows = $wpdb->get_results(
432 $wpdb->prepare(
433 "SELECT id, order_id, funnel_id, parent_id, total_sales, orderbump_sales, upsell_sales, downsell_sales, status, date_created FROM {$table} {$where} ORDER BY date_created DESC LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
434 $per_page,
435 $offset
436 ),
437 ARRAY_A
438 );
439
440 $items = array_map(
441 static function ( $row ) {
442 return [
443 'order_id' => (int) $row['order_id'],
444 'funnel_id' => (int) $row['funnel_id'],
445 'is_child_order' => ( (int) $row['parent_id'] > 0 ),
446 'parent_order_id' => (int) $row['parent_id'],
447 'total_sales' => (float) $row['total_sales'],
448 'orderbump_sales' => (float) $row['orderbump_sales'],
449 'upsell_sales' => (float) $row['upsell_sales'],
450 'downsell_sales' => (float) $row['downsell_sales'],
451 'status' => $row['status'],
452 'date_created' => $row['date_created'],
453 ];
454 },
455 is_array( $rows ) ? $rows : []
456 );
457
458 return MCPHelper::paginate( $items, $total, $page, $per_page );
459 }
460
461 /**
462 * Read recent lines from the most recently modified WPFunnels log files.
463 *
464 * @param array $input Tool input.
465 * @return array|\WP_Error
466 */
467 public static function getRecentLogs( $input = [] ) {
468 if ( ! class_exists( '\Wpfnl_Logger' ) ) {
469 return MCPHelper::error( 'logger_unavailable', 'The WPFunnels logger is not available on this install.' );
470 }
471
472 // Instantiate so the log directory constants are defined even if
473 // nothing has logged yet this request.
474 \Wpfnl_Logger::getInstance();
475
476 $limit = isset( $input['lines'] ) ? max( 1, min( 200, (int) $input['lines'] ) ) : 50;
477
478 $files = \Wpfnl_Logger::get_log_files();
479 if ( empty( $files ) || ! defined( 'WPFNL_LOG_FILE_DIR' ) ) {
480 return [ 'files' => [] ];
481 }
482
483 $mtimes = [];
484 foreach ( $files as $file ) {
485 $path = trailingslashit( WPFNL_LOG_FILE_DIR ) . $file;
486 if ( is_readable( $path ) ) {
487 $mtimes[ $path ] = filemtime( $path );
488 }
489 }
490 arsort( $mtimes );
491 $paths = array_slice( array_keys( $mtimes ), 0, 3 );
492
493 $result = [];
494 foreach ( $paths as $path ) {
495 $content = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
496 $lines = $content ? array_slice( preg_split( '/\r\n|\r|\n/', trim( $content ) ), -$limit ) : [];
497 $result[] = [
498 'file' => basename( $path ),
499 'lines' => $lines,
500 ];
501 }
502
503 return [ 'files' => $result ];
504 }
505 }
506