PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.28
Booktics – Appointment Booking Calendar for Service Businesses v1.0.28
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
← All changes | core/cart/controllers/cart-controller.php +427 -344 1.0.1 → 1.0.28 View file →
@@ -1,344 +1,427 @@
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 - $validated = booktics_validate(
107 - $data, array(
108 - 'date' => array( 'required' ),
109 - 'start_time' => array( 'required' ),
110 - 'service_id' => array( 'required' ),
111 - 'team_member_id' => array( 'required' ),
112 - )
113 - );
114 -
115 - if ( is_wp_error( $validated ) ) {
116 - return $validated;
117 - }
118 -
119 - $item_validation = $this->validate_cart_item( $data );
120 - if ( is_wp_error( $item_validation ) ) {
121 - return $item_validation;
122 - }
123 -
124 - return $item_validation;
125 - }
126 -
127 - /**
128 - * Add an item to the cart
129 - *
130 - * @param $request
131 - *
132 - * @return array|\WP_HTTP_Response
133 - */
134 - public function add_to_cart( $request ) {
135 - $params = json_decode( $request->get_body(), true );
136 - $validated = $this->prepare_item_to_save( $params );
137 -
138 - if ( is_wp_error( $validated ) ) {
139 - return $this->error(
140 - __( 'Cart validation error', 'booktics' ),
141 - 422,
142 - 'Validation',
143 - $validated->errors
144 - );
145 - }
146 - $response = $this->cart_manager->add_to_cart( $validated );
147 - $response['service'] = $this->get_service( $response['service_id'] );
148 - $response['team'] = $this->get_team_member( $response['team_member_id'] );
149 -
150 - return $this->response( $response, __( 'Item added to cart', 'booktics' ), 201 );
151 - }
152 -
153 - /**
154 - * Delete an item from the user's cart
155 - *
156 - * @param $request
157 - *
158 - * @return array|\WP_HTTP_Response
159 - */
160 - public function delete_from_cart( $request ) {
161 - $uuid = $request->get_param( 'uuid' );
162 -
163 - if ( empty( $uuid ) ) {
164 - return $this->error(
165 - __( 'UUID is required', 'booktics' ),
166 - 422,
167 - 'Validation'
168 - );
169 - }
170 -
171 - $this->cart_manager->remove_cart_item( $uuid );
172 -
173 - return $this->response( array(), __( 'Successfully deleted item from cart.', 'booktics' ) );
174 - }
175 -
176 - /**
177 - * Synchronize cart session data to database for logged-in user
178 - *
179 - * @param $request
180 - *
181 - * @return array|\WP_HTTP_Response
182 - */
183 - public function sync_cart( $request ) {
184 - $user_id = get_current_user_id();
185 - $this->cart_manager->sync_session_to_db( $user_id );
186 -
187 - $response = $this->cart_manager->get_cart();
188 -
189 - return $this->response( $response, __( 'Successfully synced cart.', 'booktics' ) );
190 - }
191 -
192 - /**
193 - * Get all items from the cart
194 - *
195 - * @return array|\WP_HTTP_Response
196 - */
197 - public function get_cart_items() {
198 - $response = $this->cart_manager->get_cart();
199 - $response['items'] = isset( $response['items'] ) ? array_map(
200 - function ( $item ) {
201 - if ( ! is_array( $item ) ) {
202 - $item = (array) $item;
203 - }
204 -
205 - $item['service'] = $this->get_service( $item['service_id'] );
206 - $item['team'] = $this->get_team_member( $item['team_member_id'] );
207 -
208 - return $item;
209 - }, $response['items']
210 - ) : array();
211 -
212 - return $this->response( $response, __( 'Cart items fetched successfully', 'booktics' ) );
213 - }
214 -
215 - /**
216 - * Check if the current user is logged in
217 - *
218 - * @return bool True if user is logged in, false otherwise
219 - */
220 - public function is_user_logged_in() {
221 - return is_user_logged_in();
222 - }
223 -
224 - /**
225 - * Validate service, team member and price
226 - *
227 - * @param array $data Cart item data
228 - *
229 - * @return array|WP_Error Returns true if valid, WP_Error otherwise
230 - */
231 - private function validate_cart_item( array $data ) {
232 - $error = new WP_Error();
233 -
234 - $service = ( new Service_Model() )->find( $data['service_id'] );
235 - if ( ! $service ) {
236 - $error->add( 'invalid_service', __( 'Invalid service selected', 'booktics' ) );
237 - }
238 -
239 - $team_member = ( new Team_Member_Model() )->find( $data['team_member_id'] );
240 - if ( empty( $team_member ) ) {
241 - $error->add( 'invalid_team_member', __( 'Invalid team member selected', 'booktics' ) );
242 - }
243 -
244 - $service_price = 0;
245 - $addition_durations = $service->additional_durations;
246 - if ( ! empty( $addition_durations ) && isset( $data['additional_duration_id'] ) ) {
247 - $addition_durations = array_filter(
248 - $addition_durations, function ( $duration ) use ( $data ) {
249 - return $duration['id'] === $data['additional_duration_id'];
250 - }
251 - );
252 - $addition_duration = reset( $addition_durations );
253 - $service_price = $addition_duration['charge_amount'] ?? 0;
254 - }
255 - $data['price'] = $service_price;
256 -
257 - return $error->has_errors() ? $error : $data;
258 - }
259 -
260 - /**
261 - * Edit a cart item
262 - *
263 - * @param $request
264 - *
265 - * @return array|\WP_HTTP_Response
266 - */
267 - public function edit_cart_item( $request ) {
268 - $uuid = $request->get_param( 'uuid' );
269 - $params = json_decode( $request->get_body(), true );
270 -
271 - if ( empty( $uuid ) ) {
272 - return $this->error(
273 - __( 'UUID is required', 'booktics' ),
274 - 422,
275 - 'Validation'
276 - );
277 - }
278 -
279 - $validated = $this->prepare_item_to_save( $params );
280 - if ( is_wp_error( $validated ) ) {
281 - return $this->error(
282 - __( 'Cart validation error', 'booktics' ),
283 - 422,
284 - 'Validation',
285 - $validated->errors
286 - );
287 - }
288 -
289 - // Add UUID to validated data
290 - $validated['uuid'] = $uuid;
291 -
292 - // Update the cart item
293 - $updated_item = $this->cart_manager->edit_cart_item( $uuid, $validated );
294 -
295 - if ( empty( $updated_item ) ) {
296 - return $this->error(
297 - __( 'Cart item not found', 'booktics' ),
298 - 404,
299 - 'NotFound'
300 - );
301 - }
302 -
303 - return $this->response(
304 - $updated_item,
305 - __( 'Successfully updated cart item', 'booktics' )
306 - );
307 - }
308 -
309 - /**
310 - * @param $service_id
311 - *
312 - * @return array
313 - */
314 - public function get_service( $service_id ): array {
315 - $service = Service_Model::find( $service_id );
316 - if ( empty( $service ) ) {
317 - return array();
318 - }
319 -
320 - return array(
321 - 'id' => $service->id,
322 - 'title' => $service->title,
323 - );
324 - }
325 -
326 - /**
327 - * @param $team_member_id
328 - *
329 - * @return array
330 - */
331 - public function get_team_member( $team_member_id ): array {
332 - $team_member = ( new Team_Member_Model() )->find( $team_member_id );
333 - if ( empty( $team_member ) ) {
334 - return array();
335 - }
336 - $team_member = $team_member->to_array();
337 -
338 - return array(
339 - 'id' => $team_member['id'],
340 - 'display_name' => $team_member['display_name'] ?? '',
341 - 'image' => $team_member['image'] ?? '',
342 - );
343 - }
344 -}
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 +}