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
booktics / core / team-member / controllers / team-member-controller.php

team-member-controller.php in Booktics – Appointment Booking Calendar for Service Businesses 1.0.28, at core/team-member/controllers/team-member-controller.php

732 lines 26.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Booktics\Team_Member\Controllers;
4
5 use Booktics\Abstracts\Base_Rest_Controller;
6 use Booktics\Abstracts\User_Model;
7 use Booktics\Models\Service_Model;
8 use Booktics\Models\Team_Member_Model;
9 use Booktics\Services\Schedule_Manager;
10 use Booktics\Services\Schedule_Record;
11 use Booktics\Team_Member\Handlers\Team_Member_Event_Handler;
12 use Exception;
13 use WP_Error;
14 use WP_HTTP_Response;
15 use WP_REST_Response;
16 use WP_REST_Server;
17
18 /**
19 * Team member controller
20 *
21 * @package Booktics/Team_Member
22 */
23 class Team_Member_Controller extends Base_Rest_Controller {
24
25 /**
26 * Endpoint namespace
27 *
28 * @var string
29 */
30 protected $namespace = 'booktics/v1';
31
32 /**
33 * Route name
34 *
35 * @var string
36 */
37 protected $base = 'team-members';
38
39 /**
40 * Register routes
41 *
42 * @return void
43 */
44 public function register_routes(): void {
45 register_rest_route(
46 $this->namespace, $this->base, array(
47 array(
48 'methods' => WP_REST_Server::CREATABLE,
49 'callback' => array( $this, 'create_item' ),
50 'permission_callback' => array(
51 $this,
52 'create_team_member_permission',
53 ),
54 ),
55 array(
56 'methods' => WP_REST_Server::READABLE,
57 'callback' => array( $this, 'get_items' ),
58 'permission_callback' => array(
59 $this,
60 'get_team_member_permission',
61 ),
62 ),
63 array(
64 'methods' => WP_REST_Server::DELETABLE,
65 'callback' => array( $this, 'delete_items' ),
66 'permission_callback' => array(
67 $this,
68 'delete_team_member_permission',
69 ),
70 ),
71 )
72 );
73
74 register_rest_route(
75 $this->namespace, $this->base.'/profile', array(
76 array(
77 'methods' => WP_REST_Server::READABLE,
78 'callback' => array( $this, 'get_profile' ),
79 'permission_callback' => array(
80 $this,
81 'get_team_member_profile_permission',
82 ),
83 ),
84 )
85 );
86
87 register_rest_route(
88 $this->namespace, '/' . $this->base . '/(?P<id>[\d]+)', array(
89 array(
90 'methods' => WP_REST_Server::EDITABLE,
91 'callback' => array( $this, 'update_item' ),
92 'permission_callback' => array(
93 $this,
94 'update_team_member_permission',
95 ),
96 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
97 ),
98 array(
99 'methods' => WP_REST_Server::DELETABLE,
100 'callback' => array( $this, 'delete_item' ),
101 'permission_callback' => array(
102 $this,
103 'delete_team_member_permission',
104 ),
105 ),
106 array(
107 'method' => WP_REST_Server::READABLE,
108 'callback' => array( $this, 'get_item' ),
109 'permission_callback' => array(
110 $this,
111 'get_team_member_permission',
112 ),
113 ),
114 )
115 );
116
117 register_rest_route(
118 $this->namespace, '/' . $this->base . '/(?P<id>[\d]+)/reinvites', array(
119 array(
120 'methods' => WP_REST_Server::READABLE,
121 'callback' => array( $this, 'reinvite' ),
122 'permission_callback' => array(
123 $this,
124 'create_team_member_permission',
125 ),
126 ),
127 )
128 );
129 }
130
131 /**
132 * Check if a given request has access to create items.
133 *
134 * @param $request
135 *
136 * @return mixed
137 */
138 public function create_team_member_permission( $request ) {
139 return current_user_can( 'manage_options' );
140 }
141
142 /**
143 * Prepare and validate team member data
144 *
145 * @param $request
146 *
147 * @return array|bool|WP_Error
148 */
149 protected function prepare_team_member_data( $request ) {
150 $input_data = json_decode( $request->get_body(), true );
151
152 $validate = booktics_validate(
153 $input_data, array(
154 'first_name' => array(
155 'required',
156 ),
157 'email' => array(
158 'required',
159 'unique:wp_users,email',
160 ),
161 )
162 );
163
164 if ( is_wp_error( $validate ) ) {
165 return $validate;
166 }
167
168 return array(
169 'first_name' => $input_data['first_name'],
170 'last_name' => $input_data['last_name'],
171 'user_email' => $input_data['email'],
172 'user_login' => User_Model::generate_username( $input_data['email'] ),
173 'phone' => $input_data['phone'],
174 'email' => $input_data['email'],
175 'status' => 'pending',
176 'enable_custom_holiday' => $input_data['enable_custom_holiday'] ?? false,
177 'enable_custom_schedule' => $input_data['enable_custom_schedule'] ?? false,
178 'enable_custom_availability' => $input_data['enable_custom_availability'] ?? false,
179 );
180 }
181
182 /**
183 * Store new team member data
184 *
185 * @param $request
186 *
187 * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response
188 */
189 public function create_item( $request ) {
190 $prepared_data = $this->prepare_team_member_data( $request );
191 if ( is_wp_error( $prepared_data ) ) {
192 return $this->error(
193 $prepared_data->get_error_message(),
194 422,
195 'Validation',
196 $prepared_data->errors
197 );
198 }
199
200 $all_team_members = ( new Team_Member_Model() )->all( array() );
201 if ( ! booktics_is_pro_active() && isset( $all_team_members['total'] ) && $all_team_members['total'] >= 3 ) {
202 return $this->error(
203 'Team member limit reached for free plan.',
204 422,
205 'Validation'
206 );
207 }
208
209 $team_member = new Team_Member_Model();
210 $created = $team_member->save( $prepared_data );
211
212 $image_url = $request->get_param( 'image' );
213 if ( $image_url ) {
214 $team_member->update( array( 'image' => esc_url_raw( $image_url ) ) );
215 }
216
217 if ( ! $created ) {
218 return $this->error( __( 'Team member create error', 'booktics' ), 500 );
219 }
220
221 do_action( 'booktics_team_member_created', $team_member, $request );
222
223 ( new Team_Member_Event_Handler() )->on_team_member_create( $created );
224
225 return $this->response( $team_member->find( $created )->to_array(), __( 'Successfully created team member', 'booktics' ), 201 );
226 }
227
228 /**
229 * Check if a given request has access to get all team member
230 *
231 * @param $request
232 *
233 * @return bool
234 */
235 public function get_team_member_permission( $request ) {
236 // Admins and team members always have access
237 if ( booktics_is_admin_or_team_member() ) {
238 return true;
239 }
240
241 $nonce = $request->get_header( 'X-WP-Nonce' );
242 if ( $nonce && wp_verify_nonce( $nonce, 'wp_rest' ) ) {
243 return true;
244 }
245
246 return new WP_Error(
247 'booktics_forbidden',
248 __( 'You are not allowed to view team member data.', 'booktics' ),
249 array( 'status' => 403 )
250 );
251 }
252
253 /**
254 * Get paginated list items for team member
255 *
256 * @param $request
257 *
258 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
259 */
260 public function get_items( $request ) {
261 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
262 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
263 $first_name = ! empty( $request['first_name'] ) ? sanitize_text_field( $request['first_name'] ) : '';
264 $last_name = ! empty( $request['last_name'] ) ? sanitize_text_field( $request['last_name'] ) : '';
265 $email = ! empty( $request['email'] ) ? sanitize_email( $request['email'] ) : '';
266 $phone = ! empty( $request['phone'] ) ? sanitize_text_field( $request['phone'] ) : '';
267 $user_name = ! empty( $request['user_name'] ) ? sanitize_text_field( $request['user_name'] ) : '';
268 $status = ! empty( $request['status'] ) ? sanitize_text_field( $request['status'] ) : '';
269 $service_id = ! empty( $request['service_id'] ) ? sanitize_text_field( $request['service_id'] ) : '';
270 $search = ! empty( $request['search'] ) ? sanitize_text_field( $request['search'] ) : '';
271 $location_id = ! empty( $request['location_id'] ) ? intval( $request['location_id'] ) : 0;
272
273 if ( $location_id ) {
274 $location_teams = get_post_meta( $location_id, 'teams', true );
275 $team_ids = array_map( 'intval', is_array( $location_teams ) ? $location_teams : array() );
276
277 $items = array();
278 foreach ( $team_ids as $tid ) {
279 $member = ( new Team_Member_Model() )->find( $tid );
280 if ( $member ) {
281 $items[] = $member->to_array();
282 }
283 }
284
285 $total = count( $items );
286 $offset = ( $paged - 1 ) * $per_page;
287 $items = array_slice( $items, $offset, $per_page );
288
289 return $this->response(
290 array(
291 'items' => $items,
292 'total' => $total,
293 'per_page' => $per_page,
294 'current_page' => $paged,
295 'last_page' => $per_page > 0 ? (int) ceil( $total / $per_page ) : 0,
296 ),
297 __( 'Team members fetched for location', 'booktics' ),
298 200
299 );
300 }
301
302 $team_members = ( new Team_Member_Model() )
303 ->when(
304 $first_name, function ( $q, $v ) {
305 $q->where( 'first_name', $v, 'LIKE' );
306 }
307 )
308 ->when(
309 $last_name, function ( $q, $v ) {
310 $q->where( 'last_name', $v, 'LIKE' );
311 }
312 )
313 ->when(
314 $email, function ( $q, $v ) {
315 $q->where( 'user_email', $v, 'LIKE' );
316 }
317 )
318 ->when(
319 $phone, function ( $q, $v ) {
320 $q->where( 'phone', $v, 'LIKE' );
321 }
322 )
323 ->when(
324 $user_name, function ( $q, $v ) {
325 $q->where( 'user_login', $v, 'LIKE' );
326 }
327 )
328 ->when(
329 $service_id, function ( $q, $v ) {
330 $q->where( 'services', $v, 'LIKE' );
331 }
332 )
333 ->when(
334 $status, function ( $q, $v ) {
335 $q->where( 'status', $v, 'LIKE' );
336 }
337 )
338 ->when(
339 $search, function ( $q, $v ) {
340 $q->where( 'first_name', $v, 'LIKE' )
341 ->or_where( 'last_name', $v, 'LIKE' )
342 ->or_where( 'phone', $v, 'LIKE' );
343 }
344 )->all(
345 array(
346 'paged' => $paged,
347 'number' => $per_page,
348 )
349 );
350
351 // Mask sensitive private information fields based on user role
352 $is_admin = current_user_can( 'manage_options' );
353 $is_team_member = booktics_is_team_member();
354 $current_user_id = get_current_user_id();
355
356 foreach ( $team_members['items'] as &$team_member ) {
357 $team_member_array = is_array( $team_member ) ? $team_member : ( is_object( $team_member ) ? get_object_vars( $team_member ) : array() );
358
359 $team_member_wp_user_id = $team_member_array['wp_user_id'] ?? null;
360 $is_viewing_self = ( (int)$team_member_wp_user_id == (int)$current_user_id );
361
362 if ( ! $is_admin && ( ! $is_team_member || ! $is_viewing_self ) ) {
363 // Mask sensitive fields with empty string for API consistency
364 $team_member_array['user_email'] = '';
365 $team_member_array['email'] = '';
366 $team_member_array['phone'] = '';
367 $team_member_array['user_login'] = '';
368 $team_member_array['wp_user_id'] = '';
369 $team_member = $team_member_array;
370 }
371 }
372
373 return $this->response( $team_members );
374 }
375
376 /**
377 * Checks if a given request has access to update a team member.
378 *
379 * @param $request
380 *
381 * @return bool
382 */
383 public function update_team_member_permission( $request ) {
384 return current_user_can( 'manage_options' );
385 }
386
387 /**
388 * Updates a single team member.
389 *
390 * @param $request
391 *
392 * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response
393 * @throws Exception
394 */
395 public function update_item( $request ) {
396 if ( ! isset( $request['id'] ) ) {
397 return $this->error( __( 'Invalid team member id', 'booktics' ), 400 );
398 }
399 $team_member = ( new Team_Member_Model( $request['id'] ) );
400
401 if ( isset( $request['password'] ) ) {
402 $current_password = sanitize_text_field( $request['current_password'] ?? '' );
403 $new_password = sanitize_text_field( $request['password'] ?? '' );
404 $confirm_password = sanitize_text_field( $request['confirm_password'] ?? '' );
405
406 $validate = booktics_validate(
407 array(
408 'current_password' => $current_password,
409 'password' => $new_password,
410 'confirm_password' => $confirm_password,
411 ), array(
412 'current_password' => array( 'required' ),
413 'password' => array( 'required' ),
414 'confirm_password' => array( 'required' ),
415 )
416 );
417
418 if ( is_wp_error( $validate ) ) {
419 return $validate;
420 }
421
422 $user = wp_get_current_user();
423
424 if ( ! wp_check_password( $current_password, $user->user_pass, $user->ID ) ) {
425 return $this->error( __( 'Current password is incorrect.', 'booktics' ), 401 );
426 }
427
428 wp_set_password( $new_password, $user->ID );
429
430 return $this->response(
431 $team_member->find( $request['id'] )->to_array(),
432 __( 'Successfully updated team member', 'booktics' )
433 );
434 }
435
436 $prepared_data = $this->prepare_team_member_update( $request );
437 if ( is_wp_error( $prepared_data ) ) {
438 return $this->error( $prepared_data->get_error_message(), 422, 'Validation', $prepared_data->errors );
439 }
440
441 $updated = $team_member->update( $prepared_data );
442 // First handle adding team member to new services
443 foreach ( $prepared_data['services'] as $service ) {
444 $service_model = Service_Model::find( $service );
445 $service_team_member = $service_model->team_member == '' ? array() : $service_model->team_member;
446 if ( ! in_array( (int) $team_member->id, $service_team_member ) ) {
447 $service_team_member[] = (int) $request['id'];
448 if ( $service_model ) {
449 $service_model->update( array( 'team_member' => $service_team_member ) );
450 }
451 }
452 }
453
454 // Then remove team member from services they're no longer assigned to
455 $services = Service_Model::query()
456 ->where( 'team_member', $request['id'], 'LIKE' )
457 ->where_not_ids( $prepared_data['services'] )
458 ->get();
459
460 foreach ( $services as $service ) {
461 if ( $service->team_member ) {
462 $service_team_members = array_diff( $service->team_member, array( (int) $request['id'] ) );
463 Service_Model::find( $service->id )->update( array( 'team_member' => $service_team_members ) );
464 }
465 }
466 $image_url = $request->get_param( 'image' );
467 if ( $image_url ) {
468 $team_member->update( array( 'image' => esc_url_raw( $image_url ) ) );
469 }
470
471 if ( ! $updated ) {
472 return $this->error( __( 'Team member update error', 'booktics' ), 500 );
473 }
474 if ( isset( $request['schedule'] ) || isset( $request['custom'] ) || isset( $request['holiday'] ) ) {
475 $input_data = json_decode( $request->get_body(), true );
476 $team_member_schedule = Schedule_Manager::make( $input_data );
477 $team_member_schedule->with_team_member_id( $request['id'] );
478 $team_member_schedule->save();
479 }
480
481 do_action( 'booktics_team_member_updated', $team_member, $request );
482
483 $team_member = $team_member->find( $request['id'] )->to_array();
484 $schedule_record = ( new Schedule_Record() )->get_schedules( $request['id'] );
485 $team_member['schedule'] = empty( $schedule_record['schedule'] )
486 ? (object) array() : $schedule_record['schedule'];
487 $team_member['custom'] = $schedule_record['custom'];
488 $team_member['holiday'] = $schedule_record['holiday'];
489
490 return $this->response(
491 $team_member,
492 __( 'Successfully updated team member', 'booktics' )
493 );
494 }
495
496 /**
497 * Prepare data for team member update
498 *
499 * @param $request
500 *
501 * @return array|bool|WP_Error
502 */
503 public function prepare_team_member_update( $request ) {
504 $input_data = json_decode( $request->get_body(), true );
505 $validation_rules = array(
506 'first_name' => array(
507 'required',
508 ),
509 'email' => array(
510 'required',
511 'unique:wp_users,email,' . $request['id'],
512 ),
513 );
514 $update_validation_rules = array();
515 $validated_data = array();
516 foreach ( $input_data as $key => $value ) {
517 $validated_data[ $key ] = $value;
518 if ( ! isset( $validation_rules[ $key ] ) ) {
519 continue;
520 }
521 $update_validation_rules[ $key ] = $validation_rules[ $key ];
522 if ( 'email' == $key ) {
523 $update_validation_rules[ $key ] = $validation_rules[ $key ];
524 $validated_data['user_email'] = sanitize_email( $value );
525 }
526 }
527 $validate = booktics_validate( $input_data, $update_validation_rules );
528
529 if ( is_wp_error( $validate ) ) {
530 return $validate;
531 }
532
533 $validated_data['services'] = ! empty( $input_data['services'] ) ? $input_data['services'] : array();
534 //$validated_data['status'] = 'active';
535
536 return $validated_data;
537 }
538
539 /**
540 * Checks if a given request has access to delete a team member.
541 *
542 * @param $request
543 *
544 * @return bool
545 */
546 public function delete_team_member_permission( $request ) {
547 return current_user_can( 'manage_options' );
548 }
549
550 /**
551 * Delete a team member
552 *
553 * @param $request
554 *
555 * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response
556 */
557 public function delete_item( $request ) {
558 $id = intval( $request['id'] );
559
560 $team_member = new Team_Member_Model( $id );
561 $previous = $team_member->find( $id );
562
563 // Remove team member from assigned services
564 $services = Service_Model::query()->where( 'team_member', $id, 'LIKE' )->get();
565 foreach ( $services as $service ) {
566 if ( $service->team_member ) {
567 $service_team_members = array_diff( $service->team_member, array( $id ) );
568 Service_Model::find( $service->id )->update( array( 'team_member' => $service_team_members ) );
569 }
570 }
571
572 $deleted = $team_member->delete();
573
574 if ( ! $deleted ) {
575 return $this->error( __( 'Team member delete error', 'booktics' ), 500 );
576 }
577
578 do_action( 'booktics_team_members_deleted', $request );
579
580 return $this->response( $previous, __( 'Successfully deleted team member', 'booktics' ) );
581 }
582
583 /**
584 * Delete multiple team member
585 *
586 * @param $request
587 *
588 * @return array|WP_Error|WP_HTTP_Response
589 */
590 public function delete_items( $request ) {
591 $ids = ! empty( $request['ids'] ) ? $request['ids'] : array();
592
593 if ( ! $ids ) {
594 return $this->error( __( 'Team member ids cannot be empty', 'booktics' ), 500 );
595 }
596 $count = 0;
597 foreach ( $ids as $id ) {
598 $team_member = new Team_Member_Model( $id );
599 if ( $team_member->delete() ) {
600 ++$count;
601 }
602 }
603 if ( 0 == $count ) {
604 return $this->error( __( 'Team member delete error', 'booktics' ), 500 );
605 }
606
607 do_action( 'booktics_team_members_deleted', $request );
608
609 return $this->response( array(), __( 'Successfully deleted team member', 'booktics' ) );
610 }
611
612 /**
613 * Get single team member
614 *
615 * @param $request
616 *
617 * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response
618 */
619 public function get_item( $request ) {
620 $id = intval( $request['id'] );
621 $team_member = new Team_Member_Model( $id );
622 $team_member = $team_member->find( $id );
623 if ( ! $team_member ) {
624 return $this->error( __( 'Team member not found', 'booktics' ), 404 );
625 }
626 $team_member = $team_member->to_array();
627 $schedule_record = ( new Schedule_Record() )->get_schedules( $request['id'] );
628 $team_member['schedule'] = ! empty( $schedule_record['schedule'] ) ? $schedule_record['schedule'] : $team_member['schedule'];
629 $team_member['custom'] = ! empty( $schedule_record['custom'] ) ? $schedule_record['custom'] : $team_member['custom'];
630 $team_member['holiday'] = ! empty( $schedule_record['holiday'] ) ? $schedule_record['holiday'] : $team_member['holiday'];
631
632 // Add filter to allow modifying team member data
633 $team_member = apply_filters( 'booktics_team_member_data', $team_member, $id );
634
635 // Mask PII fields based on user role
636 $is_admin = current_user_can( 'manage_options' );
637 $is_team_member = booktics_is_team_member();
638 $current_user_id = get_current_user_id();
639
640 // Get the team member's WP user ID to check if viewing own record
641 $team_member_wp_user_id = $team_member['wp_user_id'] ?? null;
642 $is_viewing_self = ( $team_member_wp_user_id == $current_user_id );
643
644 // If not admin and (not team member OR viewing someone else's record)
645 if ( ! $is_admin && ( ! $is_team_member || ! $is_viewing_self ) ) {
646 // Mask sensitive fields with empty string for API consistency
647 $team_member['user_email'] = '';
648 $team_member['email'] = '';
649 $team_member['phone'] = '';
650 $team_member['user_login'] = '';
651 $team_member['wp_user_id'] = '';
652 }
653
654 return $this->response( $team_member, __( 'Team member successfully fetched', 'booktics' ) );
655 }
656
657 /**
658 * Check if a given request has access to create items.
659 *
660 * @param $request
661 *
662 * @return mixed
663 */
664 public function create_team_member_schedule_permission( $request ) {
665 return current_user_can( 'manage_options' );
666 }
667
668 /**
669 * Team member reinvite
670 *
671 * @param $request
672 *
673 * @return array|WP_HTTP_Response
674 */
675 public function reinvite( $request ) {
676 $id = intval( $request['id'] );
677
678 $invite = ( new Team_Member_Event_Handler() )->on_team_member_create( $id );
679
680 if ( ! $invite ) {
681 return $this->error( __( 'Team member reinvite error', 'booktics' ) );
682 }
683
684 return $this->response( array(), __( 'Team member reinvited successfully', 'booktics' ) );
685 }
686
687 /**
688 * Check if a given request has access to get team member profile.
689 *
690 * @return bool
691 */
692 public function get_team_member_profile_permission() {
693 // Team members can view their own profile
694 if ( booktics_is_admin_or_team_member() ) {
695 return true;
696 }
697
698 return new WP_Error(
699 'booktics_forbidden',
700 __( 'You are not allowed to view team member profile.', 'booktics' ),
701 array( 'status' => 403 )
702 );
703 }
704
705 /**
706 * Get team member profile
707 *
708 * @param $request
709 *
710 * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response
711 */
712 public function get_profile( $request ) {
713 $id = get_current_user_id();
714
715 $team_member = new Team_Member_Model( $id );
716 $team_member = $team_member->find( $id );
717 if ( ! $team_member ) {
718 return $this->error( __( 'Team member not found', 'booktics' ), 404 );
719 }
720 $team_member = $team_member->to_array();
721 $schedule_record = ( new Schedule_Record() )->get_schedules( $id );
722 $team_member['schedule'] = ! empty( $schedule_record['schedule'] ) ? $schedule_record['schedule'] : $team_member['schedule'];
723 $team_member['custom'] = ! empty( $schedule_record['custom'] ) ? $schedule_record['custom'] : $team_member['custom'];
724 $team_member['holiday'] = ! empty( $schedule_record['holiday'] ) ? $schedule_record['holiday'] : $team_member['holiday'];
725
726 // Add filter to allow modifying team member data
727 $team_member = apply_filters( 'booktics_team_member_data', $team_member, $id );
728
729 return $this->response( $team_member, __( 'Team member successfully fetched', 'booktics' ) );
730 }
731 }
732