PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / trunk
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce vtrunk
3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.1 trunk 1.0.4 1.1.0 1.1.0.1 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 1.1.20 1.1.21 All 160 releases
cartflows / admin-core / api / flows.php

flows.php in CartFlows – Funnel Builder & Checkout Plugin for WooCommerce trunk, at admin-core/api/flows.php

477 lines 14.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CartFlows Flows Query.
4 *
5 * @package CartFlows
6 */
7
8 namespace CartflowsAdmin\AdminCore\Api;
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 use CartflowsAdmin\AdminCore\Api\ApiBase;
16
17 /**
18 * Class Admin_Query.
19 */
20 class Flows extends ApiBase {
21
22 /**
23 * Route base.
24 *
25 * @var string
26 */
27 protected $rest_base = '/admin/flows/';
28
29 /**
30 * Instance
31 *
32 * @access private
33 * @var object Class object.
34 * @since 1.0.0
35 */
36 private static $instance;
37
38 /**
39 * Initiator
40 *
41 * @since 1.0.0
42 * @return object initialized object of class.
43 */
44 public static function get_instance() {
45 if ( ! isset( self::$instance ) ) {
46 self::$instance = new self();
47 }
48 return self::$instance;
49 }
50
51 /**
52 * Init Hooks.
53 *
54 * @since 1.0.0
55 * @return void
56 */
57 public function register_routes() {
58
59 // Eg. http://example.local/wp-json/cartflows/v1/admin/flows/.
60 $namespace = $this->get_api_namespace();
61
62 register_rest_route(
63 $namespace,
64 $this->rest_base,
65 array(
66 array(
67 'methods' => 'POST', // WP_REST_Server::READABLE.
68 'callback' => array( $this, 'get_items' ),
69 'permission_callback' => array( $this, 'get_items_permissions_check' ),
70 'args' => array(), // get_collection_params may use.
71 ),
72 'schema' => array( $this, 'get_public_item_schema' ),
73 )
74 );
75 }
76
77 /**
78 * Get items
79 *
80 * @param WP_REST_Request $request Full details about the request.
81 * @return WP_REST_Response
82 */
83 public function get_items( $request ) {
84
85 $post_status = 'any';
86 $post_count = 10;
87
88 if ( null !== $request->get_param( 'per_page' ) ) {
89 $post_count = min( absint( $request->get_param( 'per_page' ) ), 100 );
90 }
91
92 $args = array(
93 'post_type' => CARTFLOWS_FLOW_POST_TYPE,
94 'post_status' => $post_status,
95 'orderby' => 'ID',
96 );
97
98 // checking if store checkout is available and removing it from the list of flows.
99 $store_checkout_id = intval( \Cartflows_Helper::get_global_setting( '_cartflows_store_checkout' ) );
100 if ( 0 !== $store_checkout_id ) {
101 $args['post__not_in'] = array( $store_checkout_id );
102 }
103
104 if ( null !== $request->get_param( 'paged' ) ) {
105 $args['paged'] = absint( $request->get_param( 'paged' ) );
106 }
107
108 $mode = $request->get_param( 'mode' );
109
110 if ( null !== $mode ) {
111
112 if ( 'sandbox' === $mode ) {
113 $args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
114 array(
115 'key' => 'wcf-testing',
116 'value' => 'yes',
117 ),
118 );
119 } else {
120 $args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
121 'relation' => 'OR',
122 array(
123 'key' => 'wcf-testing',
124 'value' => 'no',
125 ),
126 array(
127 'key' => 'wcf-testing',
128 'compare' => 'NOT EXISTS',
129 ),
130 );
131 }
132 }
133
134 if ( 'any' === $post_status ) {
135
136 if ( null !== $request->get_param( 's' ) ) {
137 $args['s'] = sanitize_text_field( $request->get_param( 's' ) );
138 }
139
140
141 if ( null !== $request->get_param( 'post_status' ) ) {
142
143 $status = $request->get_param( 'post_status' );
144
145 // Allowlist of valid post statuses for flows.
146 $allowed_statuses = array( 'active', 'inactive', 'publish', 'draft', 'pending', 'trash' );
147
148 if ( 'active' === $status ) {
149 $args['post_status'] = 'publish';
150 } elseif ( 'inactive' === $status ) {
151 $args['post_status'] = 'draft';
152 } elseif ( in_array( $status, $allowed_statuses, true ) ) {
153 $args['post_status'] = sanitize_text_field( $status );
154 }
155 // Invalid statuses are silently ignored, falling through to the default below.
156 }
157
158 if ( ! isset( $args['post_status'] ) || null === $request->get_param( 'post_status' ) ) {
159 $args['post_status'] = 'publish';
160 }
161
162 $start_date = $request->get_param( 'start_date' );
163 $end_date = $request->get_param( 'end_date' );
164
165 if ( ! empty( $start_date ) && ! empty( $end_date ) ) {
166 $args['date_query'] = array(
167 array(
168 'after' => sanitize_text_field( $start_date ) . ' 00:00:00',
169 'before' => sanitize_text_field( $end_date ) . ' 23:59:59',
170 'inclusive' => true,
171 'column' => 'post_date',
172 ),
173 );
174 }
175 }
176
177 if ( ! empty( $post_count ) ) {
178 $args['posts_per_page'] = $post_count;
179 }
180
181 $result = new \WP_Query( $args );
182
183 $data = array(
184 'items' => array(),
185 'pagination' => array(),
186 );
187
188 // On free, compute revenue for the whole page in a single grouped query instead of one query per flow.
189 $is_free_revenue = ! _is_cartflows_pro() && ! is_wcf_pro_plan();
190 $revenue_map = array();
191 $zero_revenue = str_replace( '&nbsp;', '', wc_price( 0 ) );
192
193 if ( $is_free_revenue && ! empty( $result->posts ) ) {
194 $revenue_map = $this->get_flows_revenue( wp_list_pluck( $result->posts, 'ID' ) );
195 }
196
197 if ( $result->have_posts() ) {
198 while ( $result->have_posts() ) {
199 $result->the_post();
200
201 global $post;
202
203 $post_data = (array) $post;
204
205 // Modify the date Format just to display it.
206 $post_data['post_modified'] = date_format( date_create( $post_data['post_modified'] ), 'yy/m/d' );
207 $post_data['post_status'] = ucwords( $post_data['post_status'] );
208
209 $first_step_url = \Cartflows_Flow_Post_Type::get_instance()->get_first_step_url( $post );
210 $view = $first_step_url ? $first_step_url : '#';
211 $edit = admin_url( 'admin.php?page=cartflows&path=flows&action=wcf-edit-flow&flow_id=' . $post->ID );
212 $delete = '#';
213 $clone = '#';
214 $export = '#';
215 $post_data['flow_test_mode'] = ( 'yes' === wcf()->options->get_flow_meta_value( $post->ID, 'wcf-testing' ) ) ? true : false;
216 $flow_steps = get_post_meta( $post->ID, 'wcf-steps', true );
217 $post_data['step_count'] = is_array( $flow_steps ) ? count( $flow_steps ) : 0;
218 $post_data['actions'] = array(
219 'view' => array(
220 'action' => 'edit',
221 'class' => '',
222 'attr' => array( 'target' => '_blank' ),
223 'text' => __( 'View', 'cartflows' ),
224 'link' => $view,
225
226 ),
227 'edit' => array(
228 'action' => 'edit',
229 'class' => '',
230 'attr' => array(),
231 'text' => __( 'Edit', 'cartflows' ),
232 'link' => $edit,
233
234 ),
235 'duplicate' => array(
236 'action' => 'clone',
237 'attr' => array(),
238 'class' => '',
239 'text' => __( 'Duplicate', 'cartflows' ),
240 'link' => $clone,
241 ),
242 'export' => array(
243 'action' => 'export',
244 'attr' => array(),
245 'class' => '',
246 'text' => __( 'Export', 'cartflows' ),
247 'link' => $export,
248 ),
249 'delete' => array(
250 'action' => 'delete',
251 'attr' => array(),
252 'class' => '',
253 'text' => __( 'Delete', 'cartflows' ),
254 'link' => $delete,
255 ),
256 );
257
258 // Fetch the revenue only for free version for the PRO it will be fetched and added by the filter later in the code.
259 if ( $is_free_revenue ) {
260 $post_data['revenue'] = isset( $revenue_map[ $post->ID ] ) ? $revenue_map[ $post->ID ] : $zero_revenue;
261 }
262
263 $data['items'][] = $post_data;
264 }
265 }
266
267 $data['found_posts'] = $result->found_posts;
268 $data['post_status'] = isset( $post_data['post_status'] ) ? $post_data['post_status'] : $args['post_status'];
269
270 $data['active_flows_count'] = intval( wp_count_posts( CARTFLOWS_FLOW_POST_TYPE )->publish );
271 $data['trash_flows_count'] = intval( wp_count_posts( CARTFLOWS_FLOW_POST_TYPE )->trash );
272 $data['draft_flows_count'] = intval( wp_count_posts( CARTFLOWS_FLOW_POST_TYPE )->draft );
273
274 $data['pagination'] = array(
275 'found_posts' => $result->found_posts,
276 'paged' => $result->query['paged'],
277 'max_pages' => $result->max_num_pages,
278 );
279
280 // Reducing count of active_flows_count if store checkout is set.
281 if ( 0 !== $store_checkout_id ) {
282 $data['active_flows_count']--;
283 }
284
285 wp_reset_postdata();
286
287 $data['status'] = true;
288
289 // Retrieve the revenue data from the PRO to display it on the flow listing page.
290 if ( _is_cartflows_pro() && is_wcf_pro_plan() ) {
291 $data = apply_filters( 'cartflows_admin_flows_page_data', $data );
292 }
293
294 if ( ! $result->have_posts() ) {
295 $data['status'] = false;
296 $response = new \WP_REST_Response( $data );
297 $response->set_status( 200 );
298 return $response;
299 }
300
301 $response = new \WP_REST_Response( $data );
302 $response->set_status( 200 );
303
304 return $response;
305 }
306
307 /**
308 * Check whether a given request has permission to read notes.
309 *
310 * @param WP_REST_Request $request Full details about the request.
311 * @return WP_Error|boolean
312 */
313 public function get_items_permissions_check( $request ) {
314
315 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
316 return new \WP_Error( 'cartflows_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'cartflows' ), array( 'status' => rest_authorization_required_code() ) );
317 }
318
319 return true;
320 }
321
322 /**
323 * Get revenue of flow.
324 *
325 * @param int $flow_id flow id.
326 * @return int
327 */
328 public function get_per_flow_revenue( $flow_id ) {
329
330 $gross_sale = 0;
331
332 // Return if WooCommerce is not active.
333 if ( ! wcf()->is_woo_active ) {
334 return $gross_sale;
335 }
336
337 // Fetch primary orders: Checkout, Order Bumps.
338 $args = array(
339 'status' => array( 'completed', 'processing', 'cancelled' ), // Accepts a string: one of 'pending', 'processing', 'on-hold', 'completed', 'refunded, 'failed', 'cancelled', or a custom order status.
340 'meta_key' => '_wcf_flow_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
341 'meta_value' => $flow_id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
342 'meta_compare' => '=', // Possible values are ‘=’, ‘!=’, ‘>’, ‘>=’, ‘<‘, ‘<=’, ‘LIKE’, ‘NOT LIKE’, ‘IN’, ‘NOT IN’, ‘BETWEEN’, ‘NOT BETWEEN’, ‘EXISTS’ (only in WP >= 3.5), and ‘NOT EXISTS’ (also only in WP >= 3.5). Values ‘REGEXP’, ‘NOT REGEXP’ and ‘RLIKE’ were added in WordPress 3.7. Default value is ‘=’.
343 'return' => 'ids', // Accepts a string: 'ids' or 'objects'. Default: 'objects'.
344 );
345
346 $orders = wc_get_orders( $args );
347
348 if ( ! empty( $orders ) && is_array( $orders ) ) {
349
350 foreach ( $orders as $order_id ) {
351
352 $order = wc_get_order( $order_id );
353 $user_id = $order->get_user_id();
354
355 // skip the orders which are placed by the user whose user role is Administrator.
356 if ( $user_id && user_can( $user_id, 'cartflows_manage_flows_steps' ) ) {
357 continue;
358 }
359
360 $order_total = $order->get_total();
361 if ( ! $order->has_status( 'cancelled' ) ) {
362 $gross_sale += (float) $order_total;
363 }
364 }
365 }
366
367 return str_replace( '&nbsp;', '', wc_price( $gross_sale ) );
368 }
369
370 /**
371 * Get gross revenue for many flows in a single grouped query.
372 *
373 * Replaces the per-flow N+1 lookup; sums completed/processing order totals
374 * grouped by `_wcf_flow_id`, excluding orders placed by flow managers.
375 *
376 * @since 3.1.3
377 *
378 * @param int[] $flow_ids Flow IDs to fetch revenue for.
379 * @return array<int,string> Map of flow ID => formatted revenue string.
380 */
381 public function get_flows_revenue( $flow_ids ) {
382
383 $revenue = array();
384
385 // Return if WooCommerce is not active.
386 if ( ! function_exists( 'WC' ) ) {
387 return $revenue;
388 }
389
390 $flow_ids = array_filter( array_map( 'absint', (array) $flow_ids ) );
391 if ( empty( $flow_ids ) ) {
392 return $revenue;
393 }
394
395 global $wpdb;
396
397 // Exclude orders placed by users who can manage flows (e.g. internal test orders).
398 $excluded_user_ids = array_map(
399 'absint',
400 get_users(
401 array(
402 'capability' => 'cartflows_manage_flows_steps',
403 'fields' => 'ID',
404 )
405 )
406 );
407
408 $decimals = wc_get_price_decimals();
409 $flow_placeholder = implode( ',', array_fill( 0, count( $flow_ids ), '%d' ) );
410 $query_args = $flow_ids;
411
412 $is_hpos = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
413
414 if ( $is_hpos ) {
415 $order_table = $wpdb->prefix . 'wc_orders';
416 $order_meta_table = $wpdb->prefix . 'wc_orders_meta';
417
418 $exclude_clause = '';
419 if ( ! empty( $excluded_user_ids ) ) {
420 $exclude_clause = ' AND o.customer_id NOT IN ( ' . implode( ',', array_fill( 0, count( $excluded_user_ids ), '%d' ) ) . ' )';
421 $query_args = array_merge( $query_args, $excluded_user_ids );
422 }
423
424 //phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
425 $rows = $wpdb->get_results(
426 $wpdb->prepare(
427 "SELECT om.meta_value AS flow_id, ROUND( SUM( o.total_amount ), $decimals ) AS revenue
428 FROM $order_table o
429 INNER JOIN $order_meta_table om ON o.id = om.order_id AND om.meta_key = '_wcf_flow_id'
430 WHERE o.type = 'shop_order'
431 AND o.status IN ( 'wc-completed', 'wc-processing' )
432 AND om.meta_value IN ( $flow_placeholder )
433 $exclude_clause
434 GROUP BY om.meta_value",
435 $query_args
436 )
437 );
438 //phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
439 } else {
440 $order_table = $wpdb->prefix . 'posts';
441 $order_meta_table = $wpdb->prefix . 'postmeta';
442
443 $exclude_clause = '';
444 if ( ! empty( $excluded_user_ids ) ) {
445 $exclude_clause = ' AND ( cu.meta_value IS NULL OR cu.meta_value NOT IN ( ' . implode( ',', array_fill( 0, count( $excluded_user_ids ), '%d' ) ) . ' ) )';
446 $query_args = array_merge( $query_args, $excluded_user_ids );
447 }
448
449 //phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
450 $rows = $wpdb->get_results(
451 $wpdb->prepare(
452 "SELECT om.meta_value AS flow_id, ROUND( SUM( m.meta_value ), $decimals ) AS revenue
453 FROM $order_table o
454 INNER JOIN $order_meta_table m ON o.ID = m.post_id AND m.meta_key = '_order_total'
455 INNER JOIN $order_meta_table om ON o.ID = om.post_id AND om.meta_key = '_wcf_flow_id'
456 LEFT JOIN $order_meta_table cu ON o.ID = cu.post_id AND cu.meta_key = '_customer_user'
457 WHERE o.post_type = 'shop_order'
458 AND o.post_status IN ( 'wc-completed', 'wc-processing' )
459 AND om.meta_value IN ( $flow_placeholder )
460 $exclude_clause
461 GROUP BY om.meta_value",
462 $query_args
463 )
464 );
465 //phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
466 }
467
468 if ( ! empty( $rows ) && is_array( $rows ) ) {
469 foreach ( $rows as $row ) {
470 $revenue[ absint( $row->flow_id ) ] = str_replace( '&nbsp;', '', wc_price( (float) $row->revenue ) );
471 }
472 }
473
474 return $revenue;
475 }
476 }
477