PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.27
Booktics – Appointment Booking Calendar for Service Businesses v1.0.27
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/order/controllers/order-controller.php +28 -143 1.0.25 → 1.0.27 View file →
@@ -2,8 +2,9 @@
2 2
3 3 namespace Booktics\Order\Controllers;
4 4
5 5 use Booktics\Abstracts\Base_Rest_Controller;
6 +use Booktics\Appointment\Services\Appointment_Validator;
6 7 use Booktics\Abstracts\Booktics_Database;
7 8 use Booktics\Abstracts\User_Model;
8 9 use Booktics\Cart\Cart_Manager;
9 10 use Booktics\Customer\Handlers\Customer_Event_Handler;
@@ -293,18 +294,32 @@
293 294 }
294 295
295 296 foreach ( $input_data['items'] as $key => $item ) {
296 297 // Validate required fields for each item
297 - $item_validate = booktics_validate(
298 - $item, array(
299 - 'date' => array( 'required' ),
300 - 'start_time' => array( 'required' ),
301 - 'end_time' => array( 'required' ),
302 - 'team_member_id' => array( 'required' ),
303 - 'service_id' => array( 'required' ),
304 - )
298 + $item_rules = array(
299 + 'date' => array( 'required' ),
300 + 'start_time' => array( 'required' ),
301 + 'end_time' => array( 'required' ),
302 + 'team_member_id' => array( 'required' ),
303 + 'service_id' => array( 'required' ),
305 304 );
306 305
306 + /**
307 + * Filters the validation rules applied to a single order item.
308 + *
309 + * Extensions that assign a team member server-side (Booktics Pro
310 + * round robin) drop the team_member_id requirement here.
311 + *
312 + * @since 1.0.26
313 + *
314 + * @param array $item_rules Validation rules.
315 + * @param array $item The order item being validated.
316 + * @param array $input_data The whole order payload.
317 + */
318 + $item_rules = apply_filters( 'booktics_order_item_validation_rules', $item_rules, $item, $input_data );
319 +
320 + $item_validate = booktics_validate( $item, $item_rules );
321 +
307 322 if ( is_wp_error( $item_validate ) ) {
308 323 return $item_validate;
309 324 }
310 325
@@ -332,22 +347,8 @@
332 347 // here and reuse the same number everywhere. Reading it separately
333 348 // let a client book N seats while paying for one.
334 349 $attendees = $this->resolve_item_attendees( $item, $input_data );
335 350
336 - // In validate_and_prepare_order_data(), after converting times:
337 - $item_with_converted_times = array_merge($item, [
338 - 'start_time' => $converted_start_time['time'],
339 - 'end_time' => $converted_end_time['time'],
340 - 'date' => gmdate('Y-m-d', strtotime($converted_start_time['date'])),
341 - 'total_attendees' => $attendees,
342 - ]);
343 -
344 - $item_timet_slot_validate = $this->validate_appointment_time_is_free($item_with_converted_times);
345 -
346 - if ( is_wp_error( $item_timet_slot_validate ) ) {
347 - return $item_timet_slot_validate;
348 - }
349 -
350 351 // Money is never taken from the request. Whatever price/subtotal/total
351 352 // the client sent is discarded and rebuilt from the service record.
352 353 $amount = $this->calculate_item_amount( $item, $attendees );
353 354 if ( is_wp_error( $amount ) ) {
@@ -494,113 +495,8 @@
494 495 return max( 1, (int) $attendees );
495 496 }
496 497
497 498 /**
498 - * Check if the requested time slot has available capacity for the given team member
499 - * Works by checking all appointments for this team member on this date and calculating remaining capacity
500 - *
501 - * @param array $item Appointment item data
502 - * @return bool|WP_Error True if time slot has capacity, WP_Error if not
503 - */
504 - private function validate_appointment_time_is_free( $item ) {
505 - try {
506 - // Convert times to DateTime objects
507 - $requested_start = new \DateTime( $item['date'] . ' ' . $item['start_time'] );
508 - $requested_end = new \DateTime( $item['date'] . ' ' . $item['end_time'] );
509 -
510 - // Get the requested attendee count (default to 1 for backward compatibility)
511 - $requested_attendees = isset( $item['total_attendees'] ) ? (int) $item['total_attendees'] : 1;
512 -
513 - // Get service to check if group booking is enabled and get max capacity
514 - $service = Service_Model::find($item['service_id']);
515 - if ( ! $service ) {
516 - return new \WP_Error( 'service_not_found', __( 'Service not found', 'booktics' ) );
517 - }
518 -
519 - $max_group_size = $this->get_service_max_group_size( $service->id );
520 -
521 -
522 - $args = [
523 - 'team_member_id' => $item['team_member_id'],
524 - 'date' => $item['date'],
525 - 'post_status' => booktics_get_option('appointment_statuses_for_blocked_slot', []),
526 - ];
527 -
528 - // Get all non-cancelled appointments for this team member on this date
529 - $appointments = Appointment_Model::all( $args );
530 -
531 - if ( ! is_array( $appointments ) || empty( $appointments ) ) {
532 - // No existing appointments, check if requested attendees fit within max capacity
533 - if ( $requested_attendees > $max_group_size ) {
534 - /* translators: %1$d = requested attendees, %2$d = maximum group size */
535 - return new \WP_Error( 'exceeds_max_capacity', sprintf( __( 'Requested attendees (%1$d) exceeds maximum group size (%2$d) for this service.', 'booktics' ), $requested_attendees, $max_group_size ) );
536 - }
537 -
538 - return true;
539 - }
540 -
541 - // Calculate total attendees for overlapping time slots
542 - $overlapping_attendees = 0;
543 -
544 - foreach ( $appointments as $appointment ) {
545 - // Skip if this is the same appointment (for updates)
546 - if ( isset($item['id']) && $appointment->ID == $item['id'] ) {
547 - continue;
548 - }
549 -
550 - // Guard against legacy/localized records whose stored date/time cannot be
551 - // parsed; skipping them keeps a single bad row from blocking every booking.
552 - try {
553 - $appt_start = new \DateTime( $appointment->date . ' ' . $appointment->start_time );
554 - $appt_end = new \DateTime( $appointment->date . ' ' . $appointment->end_time );
555 - } catch ( \Throwable $e ) {
556 - booktics_log( 'skipping appointment ' . ( $appointment->ID ?? '?' ) . ' with unparseable date/time during slot validation: ' . $e->getMessage() );
557 - continue;
558 - }
559 -
560 - // Check for time overlap
561 - if ( ! ( $requested_end <= $appt_start || $requested_start >= $appt_end ) ) {
562 - // Times overlap, add this appointment's attendees to the count
563 - $appointment_attendees = $appointment->total_attendees ?? 1;
564 - $overlapping_attendees += $appointment_attendees;
565 - }
566 - }
567 -
568 - // Check if adding the requested attendees would exceed capacity
569 - $total_after_booking = $overlapping_attendees + $requested_attendees;
570 -
571 - if ($total_after_booking > $max_group_size) {
572 - return new \WP_Error( 'insufficient_capacity', __( 'Insufficient capacity' , 'booktics' ) );
573 - }
574 -
575 - return true;
576 - } catch ( \Throwable $e ) {
577 - return new \WP_Error(
578 - 'time_validation_error',
579 - __( 'Error validating appointment time. Please try another slot.', 'booktics' )
580 - );
581 - }
582 - }
583 -
584 - /**
585 - * Get max group size for a specific service
586 - *
587 - * @param int $service_id Service ID
588 - *
589 - * @return int Max group size
590 - */
591 - private function get_service_max_group_size( $service_id ) {
592 - $default_max_size = 1;
593 - $group_booking_meta = get_post_meta( $service_id, 'group_booking', true );
594 -
595 - if ( ! $this->is_group_booking_enabled( $group_booking_meta ) ) {
596 - return $default_max_size;
597 - }
598 -
599 - return $this->extract_max_booking_size( $group_booking_meta, $default_max_size );
600 - }
601 -
602 - /**
603 499 * Checks if group booking is enabled for a services group booking meta data
604 500 *
605 501 * @param array $group_booking_meta Group booking meta data
606 502 *
@@ -610,24 +506,8 @@
610 506 return !empty( $group_booking_meta ) && is_array( $group_booking_meta ) && !empty( $group_booking_meta['enable_group_booking'] );
611 507 }
612 508
613 509 /**
614 - * Extracts the max booking size from the group booking meta data of a service
615 - *
616 - * @param array $group_booking_meta Group booking meta data
617 - * @param int $default_size Default max booking size
618 - *
619 - * @return int Max booking size
620 - */
621 - private function extract_max_booking_size( $group_booking_meta, $default_size ) {
622 - if ( isset( $group_booking_meta['max_booking'] ) && is_numeric( $group_booking_meta['max_booking'] ) ) {
623 - return (int) $group_booking_meta['max_booking'];
624 - }
625 -
626 - return $default_size;
627 - }
628 -
629 - /**
630 510 * Resolve the customer ID from input data, creating or finding the customer as needed
631 511 *
632 512 * @param array $input_data
633 513 *
@@ -719,8 +599,13 @@
719 599
720 600
721 601
722 602 $appointment_data = apply_filters( 'booktics_appointment_pro_format', $appointment_data, $item );
603 +
604 + $conflict = Appointment_Validator::validate_slot_available( $appointment_data );
605 + if ( $conflict ) {
606 + throw new Exception( esc_html( $conflict->get_error_message() ) );
607 + }
723 608
724 609 $appointment = Appointment_Model::create( $appointment_data );
725 610 if ( is_wp_error( $appointment ) ) {
726 611 throw new Exception( esc_html( $appointment->get_error_message() ) );