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 / rest / Analytics / Base.php

Base.php in Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO 1.4.14, at rest/Analytics/Base.php

558 lines 17.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php //phpcs:ignore
2
3 /**
4 * Analytics REST API — Abstract Base Class
5 *
6 * @package Disco
7 * @subpackage \Rest\Analytics
8 * @since 1.1.13
9 * @category Rest
10 */
11
12 namespace Disco\Rest\Analytics;
13
14 use Disco\Rest\Api;
15 use WP_REST_Controller;
16
17 /**
18 * Class Base
19 *
20 * Abstract base for all Analytics sub-controllers.
21 * Provides shared helpers, parameter builders, and the shared order schema
22 * that multiple sub-controllers reference.
23 *
24 * @package Disco
25 * @subpackage \Rest\Analytics
26 * @author Ohidul Islam <wahid0003@gmail.com>
27 * @link https://webappick.com
28 * @license https://opensource.org/licenses/gpl-license.php GNU Public License
29 * @category Rest
30 */
31 abstract class Base extends WP_REST_Controller { //phpcs:ignore
32
33 /**
34 * Base constructor.
35 *
36 * Sets the REST namespace and base for all analytics routes.
37 */
38 public function __construct() {
39 $this->namespace = Api::NAMESPACE_NAME . '/' . Api::VERSION;
40 $this->rest_base = 'analytics';
41 }
42
43 // =========================================================================
44 // Permissions
45 // =========================================================================
46
47 /**
48 * Checks if the current user has permission to access analytics endpoints.
49 *
50 * @param \WP_REST_Request $request Full data about the request.
51 * @return bool|\WP_Error
52 */
53 public function permissions_check( $request ) { //phpcs:ignore
54 $permission = current_user_can( 'manage_options' ) || current_user_can( 'manage_woocommerce' ); // phpcs:ignore WordPress.WP.Capabilities.Unknown
55
56 if ( ! $permission ) {
57 return new \WP_Error(
58 'disco_forbidden',
59 __( 'Sorry, Permission Denied.', 'disco' ),
60 array( 'status' => 403 )
61 );
62 }
63
64 return $permission;
65 }
66
67 // =========================================================================
68 // Shared Helpers
69 // =========================================================================
70
71 /**
72 * Validates that date_from is not after date_to.
73 *
74 * @param \WP_REST_Request $request Request object.
75 * @return true|\WP_Error
76 */
77 // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
78 protected function validate_date_params( $request ) {
79 $from = '';
80 $to = '';
81
82 if ( is_string( $request['date_from'] ) ) {
83 $from = $request['date_from'];
84 }
85
86 if ( is_string( $request['date_to'] ) ) {
87 $to = $request['date_to'];
88 }
89
90 if ( $from ) {
91 $dt = \DateTime::createFromFormat( '!Y-m-d', $from );
92
93 if ( ! $dt || $dt->format( 'Y-m-d' ) !== $from ) {
94 return new \WP_Error( 'disco_invalid_date', __( 'date_from must be a valid Y-m-d date.', 'disco' ), array( 'status' => 400 ) );
95 }
96 }
97
98 if ( $to ) {
99 $dt = \DateTime::createFromFormat( '!Y-m-d', $to );
100
101 if ( ! $dt || $dt->format( 'Y-m-d' ) !== $to ) {
102 return new \WP_Error( 'disco_invalid_date', __( 'date_to must be a valid Y-m-d date.', 'disco' ), array( 'status' => 400 ) );
103 }
104 }
105
106 if ( $from && $to && $from > $to ) {
107 return new \WP_Error(
108 'disco_invalid_date_range',
109 __( 'date_from must be on or before date_to.', 'disco' ),
110 array( 'status' => 400 )
111 );
112 }
113
114 return true;
115 }
116
117 // =========================================================================
118 // Response Builders
119 // =========================================================================
120
121 /**
122 * Builds the collection metadata object for paginated list responses.
123 *
124 * @param int $total Total number of records.
125 * @param int $per_page Items per page.
126 * @param int $current_page Current page number.
127 */
128 protected function build_collection_meta( int $total, int $per_page, int $current_page ): array {
129 $total_pages = 0;
130
131 if ( $per_page > 0 ) {
132 $total_pages = (int) ceil( $total / $per_page );
133 }
134
135 $count = max( 0, min( $per_page, $total - ( $current_page - 1 ) * $per_page ) );
136
137 return array(
138 'total' => $total,
139 'count' => $count,
140 'per_page' => $per_page,
141 'current_page' => $current_page,
142 'total_pages' => $total_pages,
143 );
144 }
145
146 /**
147 * Builds the _links object for a single item.
148 *
149 * @param string $resource_name Resource slug (e.g. 'campaigns', 'products').
150 * @param int $id Item ID.
151 */
152 protected function build_item_links( string $resource_name, int $id ): array {
153 $base = $this->namespace . '/' . $this->rest_base . '/' . $resource_name;
154
155 return array(
156 'self' => array(
157 array(
158 'href' => rest_url( $base . '/' . $id ),
159 'targetHints' => array( 'allow' => array( 'GET' ) ),
160 ),
161 ),
162 'collection' => array(
163 array(
164 'href' => rest_url( $base ),
165 ),
166 ),
167 );
168 }
169
170 /**
171 * Builds the top-level links object for a paginated list response.
172 * All existing query params are preserved across pages.
173 *
174 * @param \WP_REST_Request $request Incoming request (for query params).
175 * @param string $resource_name Resource slug.
176 * @param int $total_pages Total number of pages.
177 * @param int $current_page Current page number.
178 */
179 protected function build_top_links(
180 \WP_REST_Request $request,
181 string $resource_name,
182 int $total_pages,
183 int $current_page
184 ): array {
185 $base = rest_url( $this->namespace . '/' . $this->rest_base . '/' . $resource_name );
186 $params = $request->get_query_params();
187 unset( $params['page'] );
188
189 $make = static function ( int $page ) use ( $params, $base ): string {
190 return add_query_arg( array_merge( $params, array( 'page' => $page ) ), $base );
191 };
192
193 $links = array(
194 'self' => $make( $current_page ),
195 'first' => $make( 1 ),
196 'last' => $make( max( 1, $total_pages ) ),
197 );
198
199 if ( $current_page > 1 ) {
200 $links['prev'] = $make( $current_page - 1 );
201 }
202
203 if ( $current_page < $total_pages ) {
204 $links['next'] = $make( $current_page + 1 );
205 }
206
207 return $links;
208 }
209
210 /**
211 * Appends _links to each item in a data array.
212 *
213 * @param array $items Array of items.
214 * @param string $resource_name Resource slug.
215 * @param string $id_key Key name for the item ID (default 'id').
216 */
217 protected function add_item_links( array $items, string $resource_name, string $id_key = 'id' ): array {
218 return array_map(
219 function ( array $item ) use ( $resource_name, $id_key ): array {
220 $item['_links'] = $this->build_item_links( $resource_name, (int) ( $item[ $id_key ] ?? 0 ) );
221
222 return $item;
223 },
224 $items
225 );
226 }
227
228 // =========================================================================
229 // Parameter Builders
230 // =========================================================================
231
232 /**
233 * Returns date-only params shared by detail endpoints.
234 */
235 protected function get_date_params(): array {
236 return array(
237 'date_from' => array(
238 'description' => __( 'Filter start date (Y-m-d).', 'disco' ),
239 'type' => 'string',
240 'format' => 'date',
241 'sanitize_callback' => 'sanitize_text_field',
242 ),
243 'date_to' => array(
244 'description' => __( 'Filter end date (Y-m-d).', 'disco' ),
245 'type' => 'string',
246 'format' => 'date',
247 'sanitize_callback' => 'sanitize_text_field',
248 ),
249 );
250 }
251
252 /**
253 * Returns params for the /summary endpoint.
254 */
255 protected function get_summary_params(): array {
256 return array_merge(
257 $this->get_date_params(),
258 array(
259 'compare' => array(
260 'description' => __( 'Comparison mode. Only previous_period is supported.', 'disco' ),
261 'type' => 'string',
262 'enum' => array( 'previous_period' ),
263 'default' => 'previous_period',
264 'sanitize_callback' => 'sanitize_text_field',
265 ),
266 )
267 );
268 }
269
270 /**
271 * Returns params for the lightweight orders table endpoint.
272 */
273 // phpcs:ignore SlevomatCodingStandard.Functions.FunctionLength.FunctionLength
274 protected function get_orders_table_params(): array {
275 return array_merge(
276 $this->get_date_params(),
277 array(
278 'search' => array(
279 'description' => __( 'Search term. Numeric = exact order ID; text = customer name/email LIKE search.', 'disco' ),
280 'type' => 'string',
281 'sanitize_callback' => 'sanitize_text_field',
282 ),
283 'sort_by' => array(
284 'description' => __( 'Sort field.', 'disco' ),
285 'type' => 'string',
286 'enum' => array( 'revenue', 'date', 'quantity' ),
287 'default' => 'revenue',
288 'sanitize_callback' => 'sanitize_text_field',
289 ),
290 'campaign_id' => array(
291 'description' => __( 'Filter by campaign ID.', 'disco' ),
292 'type' => 'integer',
293 'sanitize_callback' => 'absint',
294 ),
295 'customer_id' => array(
296 'description' => __( 'Filter by customer (WP user) ID.', 'disco' ),
297 'type' => 'integer',
298 'sanitize_callback' => 'absint',
299 ),
300 'orderby' => array(
301 'description' => __( 'Sort field.', 'disco' ),
302 'type' => 'string',
303 'enum' => array( 'revenue', 'date', 'quantity' ),
304 'default' => 'revenue',
305 'sanitize_callback' => 'sanitize_text_field',
306 ),
307 'order' => array(
308 'description' => __( 'Sort direction.', 'disco' ),
309 'type' => 'string',
310 'enum' => array( 'asc', 'desc' ),
311 'default' => 'desc',
312 'sanitize_callback' => 'sanitize_text_field',
313 ),
314 'page' => array(
315 'description' => __( 'Page number.', 'disco' ),
316 'type' => 'integer',
317 'default' => 1,
318 'minimum' => 1,
319 'sanitize_callback' => 'absint',
320 ),
321 'limit' => array(
322 'description' => __( 'Results per page (max 100).', 'disco' ),
323 'type' => 'integer',
324 'default' => 10,
325 'minimum' => 1,
326 'maximum' => 100,
327 'sanitize_callback' => 'absint',
328 ),
329 )
330 );
331 }
332
333 /**
334 * Returns params for the products table endpoint.
335 */
336 // phpcs:ignore SlevomatCodingStandard.Functions.FunctionLength.FunctionLength
337 protected function get_products_table_params(): array {
338 return array_merge(
339 $this->get_date_params(),
340 array(
341 'search' => array(
342 'description' => __( 'Search term. Numeric = exact product ID; text = name LIKE search.', 'disco' ),
343 'type' => 'string',
344 'sanitize_callback' => 'sanitize_text_field',
345 ),
346 'sort_by' => array(
347 'description' => __( 'Sort field.', 'disco' ),
348 'type' => 'string',
349 'enum' => array( 'revenue', 'orders', 'customers', 'quantity' ),
350 'default' => 'revenue',
351 'sanitize_callback' => 'sanitize_text_field',
352 ),
353 'campaign_id' => array(
354 'description' => __( 'Filter by campaign ID.', 'disco' ),
355 'type' => 'integer',
356 'sanitize_callback' => 'absint',
357 ),
358 'customer_id' => array(
359 'description' => __( 'Filter by customer (WP user) ID.', 'disco' ),
360 'type' => 'integer',
361 'sanitize_callback' => 'absint',
362 ),
363 'order_id' => array(
364 'description' => __( 'Filter by WooCommerce order ID.', 'disco' ),
365 'type' => 'integer',
366 'sanitize_callback' => 'absint',
367 ),
368 'orderby' => array(
369 'description' => __( 'Sort field.', 'disco' ),
370 'type' => 'string',
371 'enum' => array( 'total_revenue', 'total_orders', 'total_customers', 'total_quantity' ),
372 'default' => 'total_revenue',
373 'sanitize_callback' => 'sanitize_text_field',
374 ),
375 'order' => array(
376 'description' => __( 'Sort direction.', 'disco' ),
377 'type' => 'string',
378 'enum' => array( 'asc', 'desc' ),
379 'default' => 'desc',
380 'sanitize_callback' => 'sanitize_text_field',
381 ),
382 'page' => array(
383 'description' => __( 'Page number.', 'disco' ),
384 'type' => 'integer',
385 'default' => 1,
386 'minimum' => 1,
387 'sanitize_callback' => 'absint',
388 ),
389 'limit' => array(
390 'description' => __( 'Results per page (max 100).', 'disco' ),
391 'type' => 'integer',
392 'default' => 10,
393 'minimum' => 1,
394 'maximum' => 100,
395 'sanitize_callback' => 'absint',
396 ),
397 )
398 );
399 }
400
401 /**
402 * Returns params for the customers table endpoint.
403 */
404 // phpcs:ignore SlevomatCodingStandard.Functions.FunctionLength.FunctionLength
405 protected function get_customers_table_params(): array {
406 return array_merge(
407 $this->get_date_params(),
408 array(
409 'search' => array(
410 'description' => __( 'Search term. Numeric = exact customer ID; text = name/email LIKE search.', 'disco' ),
411 'type' => 'string',
412 'sanitize_callback' => 'sanitize_text_field',
413 ),
414 'sort_by' => array(
415 'description' => __( 'Sort field.', 'disco' ),
416 'type' => 'string',
417 'enum' => array( 'total_spent', 'orders' ),
418 'default' => 'total_spent',
419 'sanitize_callback' => 'sanitize_text_field',
420 ),
421 'campaign_id' => array(
422 'description' => __( 'Filter by campaign ID.', 'disco' ),
423 'type' => 'integer',
424 'sanitize_callback' => 'absint',
425 ),
426 'order_id' => array(
427 'description' => __( 'Filter by WooCommerce order ID.', 'disco' ),
428 'type' => 'integer',
429 'sanitize_callback' => 'absint',
430 ),
431 'orderby' => array(
432 'description' => __( 'Sort field.', 'disco' ),
433 'type' => 'string',
434 'enum' => array( 'total_spent', 'orders' ),
435 'default' => 'total_spent',
436 'sanitize_callback' => 'sanitize_text_field',
437 ),
438 'order' => array(
439 'description' => __( 'Sort direction.', 'disco' ),
440 'type' => 'string',
441 'enum' => array( 'asc', 'desc' ),
442 'default' => 'asc',
443 'sanitize_callback' => 'sanitize_text_field',
444 ),
445 'page' => array(
446 'description' => __( 'Page number.', 'disco' ),
447 'type' => 'integer',
448 'default' => 1,
449 'minimum' => 1,
450 'sanitize_callback' => 'absint',
451 ),
452 'limit' => array(
453 'description' => __( 'Results per page (max 100).', 'disco' ),
454 'type' => 'integer',
455 'default' => 10,
456 'minimum' => 1,
457 'maximum' => 100,
458 'sanitize_callback' => 'absint',
459 ),
460 )
461 );
462 }
463
464 // =========================================================================
465 // Shared Schema
466 // =========================================================================
467
468 /**
469 * Retrieves the order analytics schema, conforming to JSON Schema.
470 * Shared by CampaignsApi, OrdersApi, and CustomersApi.
471 *
472 * @return array
473 */
474 public function get_order_schema() { //phpcs:ignore
475 return array(
476 '$schema' => 'http://json-schema.org/draft-04/schema#',
477 'title' => 'analytics-order',
478 'type' => 'object',
479 'properties' => array(
480 'id' => array(
481 'description' => __( 'Order ID.', 'disco' ),
482 'type' => 'integer',
483 'context' => array( 'view' ),
484 'readonly' => true,
485 ),
486 'order_date' => array(
487 'description' => __( 'Order date.', 'disco' ),
488 'type' => 'string',
489 'format' => 'date-time',
490 'context' => array( 'view' ),
491 'readonly' => true,
492 ),
493 'order_status' => array(
494 'description' => __( 'Order status.', 'disco' ),
495 'type' => 'string',
496 'context' => array( 'view' ),
497 'readonly' => true,
498 ),
499 'customer_id' => array(
500 'description' => __( 'WP User ID.', 'disco' ),
501 'type' => 'integer',
502 'context' => array( 'view' ),
503 'readonly' => true,
504 ),
505 'customer_name' => array(
506 'description' => __( 'Customer display name.', 'disco' ),
507 'type' => 'string',
508 'context' => array( 'view' ),
509 'readonly' => true,
510 ),
511 'customer_email' => array(
512 'description' => __( 'Customer email.', 'disco' ),
513 'type' => 'string',
514 'context' => array( 'view' ),
515 'readonly' => true,
516 ),
517 'campaigns' => array(
518 'description' => __( 'Campaigns used in this order.', 'disco' ),
519 'type' => 'array',
520 'context' => array( 'view' ),
521 'readonly' => true,
522 ),
523 'items_count' => array(
524 'description' => __( 'Number of line items.', 'disco' ),
525 'type' => 'integer',
526 'context' => array( 'view' ),
527 'readonly' => true,
528 ),
529 'order_total' => array(
530 'description' => __( 'Order total.', 'disco' ),
531 'type' => 'number',
532 'context' => array( 'view' ),
533 'readonly' => true,
534 ),
535 'total_spent' => array(
536 'description' => __( 'Order total (order-list endpoint).', 'disco' ),
537 'type' => 'number',
538 'context' => array( 'view' ),
539 'readonly' => true,
540 ),
541 'discount_amount' => array(
542 'description' => __( 'Discount amount.', 'disco' ),
543 'type' => 'number',
544 'context' => array( 'view' ),
545 'readonly' => true,
546 ),
547 'products' => array(
548 'description' => __( 'Line items (single-order endpoint only).', 'disco' ),
549 'type' => 'array',
550 'context' => array( 'view' ),
551 'readonly' => true,
552 ),
553 ),
554 );
555 }
556
557 }
558