PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.14
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.14
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / includes / API / Coupons_Controller.php

Coupons_Controller.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.9.14, at includes/API/Coupons_Controller.php

363 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Coupons_Controller.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS\API;
9
10 \defined( 'ABSPATH' ) || die;
11
12 if ( ! class_exists( 'WC_REST_Coupons_Controller' ) ) {
13 return;
14 }
15
16 use Exception;
17 use Ramsey\Uuid\Uuid;
18 use WC_Coupon;
19 use WC_Meta_Data;
20 use WC_REST_Coupons_Controller;
21 use WCPOS\WooCommercePOS\Logger;
22 use WP_Error;
23 use WP_REST_Request;
24 use WP_REST_Response;
25
26 /**
27 * Coupons controller class.
28 *
29 * Extends WC_REST_Coupons_Controller directly under the wcpos/v1
30 * namespace and adds POS-specific behaviour (UUID, permissions,
31 * optimised bulk-ID queries).
32 */
33 class Coupons_Controller extends WC_REST_Coupons_Controller {
34 use Traits\Uuid_Handler;
35 use Traits\WCPOS_REST_API;
36
37 /**
38 * Endpoint namespace.
39 *
40 * @var string
41 */
42 protected $namespace = 'wcpos/v1';
43
44 /**
45 * Store the request object for use in lifecycle methods.
46 *
47 * @var WP_REST_Request
48 */
49 protected $wcpos_request;
50
51 /**
52 * Dispatch request to parent controller, or override if needed.
53 *
54 * @param mixed $dispatch_result Dispatch result, will be used if not empty.
55 * @param WP_REST_Request $request Request used to generate the response.
56 * @param string $route Route matched for the request.
57 * @param array $handler Route handler used for the request.
58 */
59 public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $request, $route, $handler ) {
60 $this->wcpos_request = $request;
61
62 add_filter( 'woocommerce_rest_prepare_shop_coupon_object', array( $this, 'wcpos_coupon_response' ), 10, 3 );
63 add_filter( 'woocommerce_rest_check_permissions', array( $this, 'wcpos_check_permissions' ), 10, 4 );
64 add_action( 'woocommerce_update_coupon', array( $this, 'wcpos_touch_coupon_modified_date' ), 10, 1 );
65
66 /**
67 * Check if the request is for all coupons and if the 'posts_per_page' is set to -1.
68 * Optimised query for getting all coupon IDs.
69 */
70 $fields = $request->get_param( 'fields' );
71 $supports_fast_path = \is_array( $fields )
72 && ( array( 'id' ) === $fields || array( 'id', 'date_modified_gmt' ) === $fields );
73 if ( -1 === (int) $request->get_param( 'posts_per_page' ) && $supports_fast_path ) {
74 return $this->wcpos_get_all_posts( $request );
75 }
76
77 return $dispatch_result;
78 }
79
80 /**
81 * Check whether a given request has permission to read coupons.
82 *
83 * @param WP_REST_Request $request Full details about the request.
84 *
85 * @return WP_Error|boolean
86 */
87 public function get_items_permissions_check( $request ) {
88 if ( current_user_can( 'access_woocommerce_pos' ) ) {
89 return true;
90 }
91
92 return parent::get_items_permissions_check( $request );
93 }
94
95 /**
96 * Check if a given request has access to read a coupon.
97 *
98 * @param WP_REST_Request $request Full details about the request.
99 *
100 * @return WP_Error|boolean
101 */
102 public function get_item_permissions_check( $request ) {
103 if ( current_user_can( 'access_woocommerce_pos' ) ) {
104 return true;
105 }
106
107 return parent::get_item_permissions_check( $request );
108 }
109
110 /**
111 * Authorize coupon read access for POS users.
112 *
113 * The WC CRUD controller's get_items() calls wc_rest_check_post_permissions()
114 * per coupon. This filter ensures POS users can read coupons.
115 *
116 * @param bool $permission The current permission.
117 * @param string $context The context of the request (read, create, edit, delete).
118 * @param int $object_id The object ID.
119 * @param string $post_type The post type.
120 *
121 * @return bool
122 */
123 public function wcpos_check_permissions( $permission, $context, $object_id, $post_type ) {
124 if ( ! $permission && 'shop_coupon' === $post_type && 'read' === $context ) {
125 $permission = current_user_can( 'access_woocommerce_pos' );
126 }
127
128 return $permission;
129 }
130
131 /**
132 * Touch post_modified_gmt after a coupon update.
133 *
134 * WC's coupon data store only calls wp_update_post() when certain post
135 * fields (code, description, status, dates) change. When only meta-based
136 * fields like amount change, post_modified_gmt is left stale. This breaks
137 * client-side sync that relies on date_modified_gmt to detect changes.
138 *
139 * @see https://github.com/wcpos/woocommerce-pos-pro/issues/86
140 *
141 * @param int $coupon_id The coupon post ID.
142 */
143 public function wcpos_touch_coupon_modified_date( int $coupon_id ): void {
144 global $wpdb;
145
146 $now_gmt = current_time( 'mysql', true );
147 $now_local = current_time( 'mysql' );
148
149 $wpdb->update(
150 $wpdb->posts,
151 array(
152 'post_modified' => $now_local,
153 'post_modified_gmt' => $now_gmt,
154 ),
155 array( 'ID' => $coupon_id ),
156 array( '%s', '%s' ),
157 array( '%d' )
158 );
159
160 clean_post_cache( $coupon_id );
161 }
162
163 /**
164 * Get the query params for collections.
165 *
166 * @return array $params The collection parameters.
167 */
168 public function get_collection_params() {
169 $params = parent::get_collection_params();
170
171 // Ensure 'orderby' is set and is an array before attempting to modify it.
172 if ( isset( $params['orderby']['enum'] ) && \is_array( $params['orderby']['enum'] ) ) {
173 $params['orderby']['enum'] = array_unique( array_merge( $params['orderby']['enum'], array( 'code' ) ) );
174 }
175
176 return $params;
177 }
178
179 /**
180 * Prepare objects query.
181 *
182 * @param WP_REST_Request $request Full details about the request.
183 *
184 * @return array|WP_Error
185 */
186 protected function prepare_objects_query( $request ) {
187 $args = parent::prepare_objects_query( $request );
188
189 // Coupon code is stored as post_title.
190 if ( isset( $request['orderby'] ) && 'code' === $request['orderby'] ) {
191 $args['orderby'] = 'title';
192 }
193
194 if ( ! empty( $request['wcpos_include'] ) ) {
195 $args['post__in'] = array_map( 'intval', (array) $request['wcpos_include'] );
196 }
197
198 if ( ! empty( $request['wcpos_exclude'] ) ) {
199 $args['post__not_in'] = array_map( 'intval', (array) $request['wcpos_exclude'] );
200 }
201
202 return $args;
203 }
204
205 /**
206 * Filter coupon object returned from the REST API.
207 *
208 * @param WP_REST_Response $response The response object.
209 * @param WC_Coupon $coupon Coupon object used to create response.
210 * @param WP_REST_Request $request Request object.
211 *
212 * @return WP_REST_Response
213 */
214 public function wcpos_coupon_response( WP_REST_Response $response, WC_Coupon $coupon, WP_REST_Request $request ): WP_REST_Response {
215 $data = $response->get_data();
216
217 // Add the UUID to the coupon response.
218 // NOTE: WC_Coupon does not implement get_type(), so we cannot use
219 // the shared maybe_add_post_uuid() from Uuid_Handler.
220 $this->maybe_add_coupon_uuid( $coupon );
221
222 // Parse the meta data before returning the response.
223 $data['meta_data'] = $this->wcpos_parse_meta_data( $coupon );
224
225 // Estimate response size and log if excessive.
226 $this->wcpos_estimate_response_size( $data, $coupon->get_id(), 'Coupon' );
227
228 // Set changes to the response data.
229 $response->set_data( $data );
230
231 return $response;
232 }
233
234 /**
235 * Ensure the coupon has a valid UUID.
236 *
237 * WC_Coupon does not implement get_type() (unlike WC_Product and WC_Order),
238 * so we cannot use the shared maybe_add_post_uuid() from Uuid_Handler.
239 * This method provides equivalent functionality for coupons.
240 *
241 * @param WC_Coupon $coupon The coupon object.
242 */
243 private function maybe_add_coupon_uuid( WC_Coupon $coupon ): void {
244 $meta_data = $coupon->get_meta_data();
245 $uuids = array_filter(
246 $meta_data,
247 static function ( WC_Meta_Data $meta ): bool {
248 $meta_row = $meta->get_data();
249 return '_woocommerce_pos_uuid' === ( $meta_row['key'] ?? '' );
250 }
251 );
252
253 $uuid_values = array_map(
254 static function ( WC_Meta_Data $meta ) {
255 $meta_row = $meta->get_data();
256 return $meta_row['value'] ?? null;
257 },
258 $uuids
259 );
260
261 // Re-index to ensure sequential keys.
262 $uuid_values = array_values( $uuid_values );
263
264 $did_prune_duplicates = false;
265
266 // If more than one UUID exists, keep the first and delete the rest.
267 if ( count( $uuid_values ) > 1 ) {
268 $first_uuid_key = key( $uuids );
269 foreach ( $uuids as $key => $uuid_meta ) {
270 if ( $key === $first_uuid_key ) {
271 continue;
272 }
273 $uuid_meta_row = $uuid_meta->get_data();
274 if ( isset( $uuid_meta_row['id'] ) ) {
275 $coupon->delete_meta_data_by_mid( (int) $uuid_meta_row['id'] );
276 $did_prune_duplicates = true;
277 }
278 }
279 $first_uuid = reset( $uuids );
280 $first_row = $first_uuid ? $first_uuid->get_data() : array();
281 $uuid_values = array( $first_row['value'] ?? '' );
282 }
283
284 $should_update_uuid = empty( $uuid_values )
285 || ( isset( $uuid_values[0] ) && ! Uuid::isValid( $uuid_values[0] ) );
286
287 if ( $should_update_uuid ) {
288 $coupon->update_meta_data( '_woocommerce_pos_uuid', $this->create_uuid() );
289 }
290
291 if ( $did_prune_duplicates || $should_update_uuid ) {
292 $coupon->save_meta_data();
293 }
294 }
295
296 /**
297 * Returns array of all coupon IDs.
298 *
299 * @param WP_REST_Request $request Full details about the request.
300 *
301 * @return WP_REST_Response|WP_Error
302 */
303 public function wcpos_get_all_posts( $request ) {
304 global $wpdb;
305
306 // Start timing execution.
307 $start_time = microtime( true );
308
309 $modified_after = $request->get_param( 'modified_after' );
310 $fields = $request->get_param( 'fields' );
311 $id_with_modified_date = array( 'id', 'date_modified_gmt' ) === $fields;
312 $select_fields = $id_with_modified_date ? 'ID as id, post_modified_gmt as date_modified_gmt' : 'ID as id';
313
314 $sql = "SELECT DISTINCT {$select_fields} FROM {$wpdb->posts}";
315 $sql .= " WHERE post_type = 'shop_coupon' AND post_status = 'publish'";
316
317 // Add modified_after condition if provided.
318 if ( $modified_after ) {
319 $timestamp = strtotime( $modified_after );
320 if ( false === $timestamp ) {
321 return new \WP_Error(
322 'woocommerce_pos_rest_invalid_modified_after',
323 'Invalid modified_after parameter.',
324 array( 'status' => 400 )
325 );
326 }
327 $modified_after_date = gmdate( 'Y-m-d H:i:s', $timestamp );
328 $sql .= $wpdb->prepare( ' AND post_modified_gmt > %s', $modified_after_date );
329 }
330
331 // Order by post_date DESC to maintain order consistency.
332 $sql .= " ORDER BY {$wpdb->posts}.post_date DESC";
333
334 try {
335 $results = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is built with prepare() above.
336 $formatted_results = $this->wcpos_format_all_posts_response( $results );
337
338 // Get the total number of coupons for the given criteria.
339 $total = \count( $formatted_results );
340
341 // Collect execution time and server load.
342 $execution_time = microtime( true ) - $start_time;
343 $execution_time_ms = number_format( $execution_time * 1000, 2 );
344 $server_load = $this->get_server_load();
345
346 $response = rest_ensure_response( $formatted_results );
347 $response->header( 'X-WP-Total', (string) $total );
348 $response->header( 'X-Execution-Time', $execution_time_ms . ' ms' );
349 $response->header( 'X-Server-Load', json_encode( $server_load ) );
350
351 return $response;
352 } catch ( Exception $e ) {
353 Logger::log( 'Error fetching coupon IDs: ' . $e->getMessage() );
354
355 return new WP_Error(
356 'woocommerce_pos_rest_cannot_fetch',
357 'Error fetching coupon IDs.',
358 array( 'status' => 500 )
359 );
360 }
361 }
362 }
363