PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.29
Booktics – Appointment Booking Calendar for Service Businesses v1.0.29
1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.2 1.0.3 All 31 releases
booktics / core / cart / controllers / cart-controller.php

cart-controller.php in Booktics – Appointment Booking Calendar for Service Businesses 1.0.29, at core/cart/controllers/cart-controller.php

428 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Booktics\Cart\Controllers;
4
5 use Booktics\Abstracts\Base_Rest_Controller;
6 use Booktics\Cart\Cart_Manager;
7 use Booktics\Models\Service_Model;
8 use Booktics\Models\Team_Member_Model;
9 use WP_Error;
10 use WP_REST_Response;
11 use WP_REST_Server;
12
13 class Cart_Controller extends Base_Rest_Controller {
14
15 /**
16 * Endpoint namespace
17 *
18 * @var string
19 */
20 protected $namespace = 'booktics/v1';
21
22 /**
23 * Route name
24 *
25 * @var string
26 */
27 protected $base = 'carts';
28
29 /**
30 * @var Cart_Manager
31 */
32 private $cart_manager;
33
34 /**
35 * Initialize CartController with a new CartManager instance
36 */
37 public function __construct() {
38 $this->cart_manager = new Cart_Manager();
39 }
40
41 /**
42 * Register REST API routes for cart functionality
43 *
44 * @return void
45 */
46 public function register_routes(): void {
47 register_rest_route(
48 $this->namespace,
49 $this->base,
50 array(
51 'methods' => WP_REST_Server::CREATABLE,
52 'callback' => array( $this, 'add_to_cart' ),
53 'permission_callback' => '__return_true',
54 )
55 );
56
57 register_rest_route(
58 $this->namespace,
59 $this->base . '/(?P<uuid>[a-zA-Z0-9-]+)',
60 array(
61 'methods' => WP_REST_Server::DELETABLE,
62 'callback' => array( $this, 'delete_from_cart' ),
63 'permission_callback' => '__return_true',
64 )
65 );
66
67 register_rest_route(
68 $this->namespace,
69 $this->base,
70 array(
71 'methods' => WP_REST_Server::EDITABLE,
72 'callback' => array( $this, 'sync_cart' ),
73 'permission_callback' => array( $this, 'is_user_logged_in' ),
74 )
75 );
76
77 register_rest_route(
78 $this->namespace,
79 $this->base . '/(?P<uuid>[a-zA-Z0-9-]+)',
80 array(
81 'methods' => WP_REST_Server::EDITABLE,
82 'callback' => array( $this, 'edit_cart_item' ),
83 'permission_callback' => '__return_true',
84 )
85 );
86
87 register_rest_route(
88 $this->namespace,
89 $this->base,
90 array(
91 'methods' => WP_REST_Server::READABLE,
92 'callback' => array( $this, 'get_cart_items' ),
93 'permission_callback' => '__return_true',
94 )
95 );
96 }
97
98 /**
99 * Prepare and validate cart item data
100 *
101 * @param array $data
102 *
103 * @return []|array|bool|WP_Error
104 */
105 private function prepare_item_to_save( array $data ) {
106 $item_rules = array(
107 'date' => array( 'required' ),
108 'start_time' => array( 'required' ),
109 'service_id' => array( 'required' ),
110 'team_member_id' => array( 'required' ),
111 );
112
113 /**
114 * Filters the validation rules applied to a cart item.
115 *
116 * Extensions that assign a team member server-side (Booktics Pro
117 * round robin) drop the team_member_id requirement here.
118 *
119 * @since 1.0.26
120 *
121 * @param array $item_rules Validation rules.
122 * @param array $data The cart item being validated.
123 */
124 $item_rules = apply_filters( 'booktics_cart_item_validation_rules', $item_rules, $data );
125
126 $validated = booktics_validate( $data, $item_rules );
127
128 if ( is_wp_error( $validated ) ) {
129 return $validated;
130 }
131
132 $item_validation = $this->validate_cart_item( $data );
133 if ( is_wp_error( $item_validation ) ) {
134 return $item_validation;
135 }
136
137 return $item_validation;
138 }
139
140 /**
141 * Add an item to the cart
142 *
143 * @param $request
144 *
145 * @return array|\WP_HTTP_Response
146 */
147 public function add_to_cart( $request ) {
148 $params = json_decode( $request->get_body(), true );
149 $validated = $this->prepare_item_to_save( $params );
150
151 if ( is_wp_error( $validated ) ) {
152 return $this->error(
153 __( 'Cart validation error', 'booktics' ),
154 422,
155 'Validation',
156 $validated->errors
157 );
158 }
159 // Calculate end time from start time and total duration
160 $total_duration_min = $this->calculate_total_duration( $validated );
161 $validated['end_time'] = $this->get_endtime_from_starttime($validated['start_time'], $total_duration_min);
162
163 $response = $this->cart_manager->add_to_cart( $validated );
164 $response['service'] = $this->get_service( $response['service_id'] );
165 $response['team'] = $this->get_team_member( $response['team_member_id'] );
166
167 // Allow booktics-pro to add location details
168 $response = apply_filters( 'booktics_cart_item_response', $response );
169
170 return $this->response( $response, __( 'Item added to cart', 'booktics' ), 201 );
171 }
172
173 /**
174 * Delete an item from the user's cart
175 *
176 * @param $request
177 *
178 * @return array|\WP_HTTP_Response
179 */
180 public function delete_from_cart( $request ) {
181 $uuid = sanitize_text_field( $request->get_param( 'uuid' ) );
182
183 if ( empty( $uuid ) ) {
184 return $this->error(
185 __( 'UUID is required', 'booktics' ),
186 422,
187 'Validation'
188 );
189 }
190
191 $this->cart_manager->remove_cart_item( $uuid );
192
193 return $this->response( array(), __( 'Successfully deleted item from cart.', 'booktics' ) );
194 }
195
196 /**
197 * Synchronize cart session data to database for logged-in user
198 *
199 * @param $request
200 *
201 * @return array|\WP_HTTP_Response
202 */
203 public function sync_cart( $request ) {
204 $user_id = get_current_user_id();
205 $this->cart_manager->sync_session_to_db( $user_id );
206
207 $response = $this->cart_manager->get_cart();
208
209 return $this->response( $response, __( 'Successfully synced cart.', 'booktics' ) );
210 }
211
212 /**
213 * Get all items from the cart
214 *
215 * @return array|\WP_HTTP_Response
216 */
217 public function get_cart_items() {
218 $response = $this->cart_manager->get_cart();
219 $response['items'] = isset( $response['items'] ) ? array_map(
220 function ( $item ) {
221 if ( ! is_array( $item ) ) {
222 $item = (array) $item;
223 }
224
225 $item['service'] = $this->get_service( $item['service_id'] );
226 $item['team'] = $this->get_team_member( $item['team_member_id'] );
227
228 // Allow booktics-pro to add location details
229 $item = apply_filters( 'booktics_cart_item_response', $item );
230
231 return $item;
232 }, $response['items']
233 ) : array();
234
235 return $this->response( $response, __( 'Cart items fetched successfully', 'booktics' ) );
236 }
237
238 /**
239 * Check if the current user is logged in
240 *
241 * @return bool True if user is logged in, false otherwise
242 */
243 public function is_user_logged_in() {
244 return is_user_logged_in();
245 }
246
247 /**
248 * Validate service, team member and price
249 *
250 * @param array $data Cart item data
251 *
252 * @return array|WP_Error Returns true if valid, WP_Error otherwise
253 */
254 private function validate_cart_item( array $data ) {
255 $error = new WP_Error();
256
257 $service = ( new Service_Model() )->find( $data['service_id'] );
258 if ( ! $service ) {
259 $error->add( 'invalid_service', __( 'Invalid service selected', 'booktics' ) );
260 }
261
262 // A cart item may legitimately arrive without a team member when the
263 // service assigns one server-side. Only verify a member that was
264 // actually supplied; the booking is refused later if none can be found.
265 if ( ! empty( $data['team_member_id'] ) ) {
266 $team_member = ( new Team_Member_Model() )->find( $data['team_member_id'] );
267 if ( empty( $team_member ) ) {
268 $error->add( 'invalid_team_member', __( 'Invalid team member selected', 'booktics' ) );
269 }
270 }
271
272 $service_price = 0;
273 $addition_durations = $service->additional_durations;
274 if ( ! empty( $addition_durations ) && isset( $data['additional_duration_id'] ) ) {
275 $addition_durations = array_filter(
276 $addition_durations, function ( $duration ) use ( $data ) {
277 return $duration['id'] === $data['additional_duration_id'];
278 }
279 );
280 $addition_duration = reset( $addition_durations );
281 $service_price = $addition_duration['charge_amount'] ?? 0;
282 }
283 $data['price'] = $service_price;
284
285 return $error->has_errors() ? $error : $data;
286 }
287
288 /**
289 * Edit a cart item
290 *
291 * @param $request
292 *
293 * @return array|\WP_HTTP_Response
294 */
295 public function edit_cart_item( $request ) {
296 $uuid = sanitize_text_field( (string) $request->get_param( 'uuid' ) );
297 $params = json_decode( $request->get_body(), true );
298
299 if ( empty( $uuid ) ) {
300 return $this->error(
301 __( 'UUID is required', 'booktics' ),
302 422,
303 'Validation'
304 );
305 }
306
307 $validated = $this->prepare_item_to_save( $params );
308 if ( is_wp_error( $validated ) ) {
309 return $this->error(
310 __( 'Cart validation error', 'booktics' ),
311 422,
312 'Validation',
313 $validated->errors
314 );
315 }
316
317 // Add UUID to validated data
318 $validated['uuid'] = $uuid;
319
320 // Calculate end time from start time and total duration
321 $total_duration_min = $this->calculate_total_duration( $validated );
322 $validated['end_time'] = $this->get_endtime_from_starttime( $validated['start_time'], $total_duration_min );
323
324 // Update the cart item
325 $updated_item = $this->cart_manager->edit_cart_item( $uuid, $validated );
326
327 if ( empty( $updated_item ) ) {
328 return $this->error(
329 __( 'Cart item not found', 'booktics' ),
330 404,
331 'NotFound'
332 );
333 }
334
335 // Allow booktics-pro to add location details
336 $updated_item = apply_filters( 'booktics_cart_item_response', $updated_item );
337
338 return $this->response(
339 $updated_item,
340 __( 'Successfully updated cart item', 'booktics' )
341 );
342 }
343
344 /**
345 * @param $service_id
346 *
347 * @return array
348 */
349 public function get_service( $service_id ): array {
350 $service = Service_Model::find( $service_id );
351 if ( empty( $service ) ) {
352 return array();
353 }
354
355 return array(
356 'id' => $service->id,
357 'title' => $service->title,
358 );
359 }
360
361 /**
362 * @param $team_member_id
363 *
364 * @return array
365 */
366 public function get_team_member( $team_member_id ): array {
367 $team_member = ( new Team_Member_Model() )->find( $team_member_id );
368 if ( empty( $team_member ) ) {
369 return array();
370 }
371 $team_member = $team_member->to_array();
372
373 return array(
374 'id' => $team_member['id'],
375 'display_name' => $team_member['display_name'] ?? '',
376 'image' => $team_member['image'] ?? '',
377 );
378 }
379
380 /**
381 * Calculate total duration for a cart item (appointment)
382 *
383 * @param array $item Cart item data
384 *
385 * @return int Total duration in minutes
386 */
387 private function calculate_total_duration( $item ) {
388 $service_model = Service_Model::find( $item['service_id'] );
389 if ( ! $service_model ) {
390 return 0;
391 }
392 $service_duration = array_filter(
393 $service_model->additional_durations, function ( $duration ) use ( $item ) {
394 return $item['additional_duration_id'] === $duration['id'];
395 }
396 );
397 if ( empty( $service_duration ) ) {
398 return 0;
399 }
400 $service_duration = reset( $service_duration );
401 $total_duration_min = $service_duration['duration_unit'] === 'min'
402 ? $service_duration['duration_time']
403 : ( $service_duration['duration_time'] * 60 );
404 if ( ! empty( $item['extra_services'] ) ) {
405 $total_duration_min = apply_filters( 'booktics_extra_service_duration', $total_duration_min, $item['extra_services'] );
406 }
407
408 $total_duration_min = apply_filters( 'booktics_calculate_total_duration', $total_duration_min, $item );
409
410 return $total_duration_min;
411 }
412
413 /**
414 * Get end time for appointments from start time and total duration
415 *
416 * @param $start_time start time of the appointment
417 * @param $total_duration_min total duration of the appointment in minutes including service time and extra service time
418 *
419 * @return string end time of the appointment
420 */
421 private function get_endtime_from_starttime( $start_time, $total_duration_min ) {
422 $start_time = strtotime( $start_time );
423 $end_time = strtotime( '+' . $total_duration_min . ' minutes', $start_time );
424
425 return gmdate( 'H:i', $end_time );
426 }
427 }
428