add( 'missing_field', sprintf( __( '%s is required', 'booktics' ), $field ) ); } } if ( empty( $data['customer_id'] ) && empty( $data['customer']['email'] ) ) { $error->add( 'missing_field', __( 'Customer email is required', 'booktics' ) ); } if ( empty( $data['items'] ) || ! is_array( $data['items'] ) ) { $error->add( 'invalid_items', __( 'Order must contain at least one item', 'booktics' ) ); return $error; } foreach ( $data['items'] as $item ) { $item_validation = $this->validate_order_item( $item ); if ( is_wp_error( $item_validation ) ) { return $item_validation; } } return $error->has_errors() ? $error : true; } /** * Validate individual order item * * @param array $item Order item data * * @return true|WP_Error Returns true if valid, WP_Error otherwise */ private function validate_order_item( array $item ) { $error = new WP_Error(); $service = ( new Service_Model() )->find( $item['service_id'] ); if ( ! $service ) { $error->add( 'invalid_service', __( 'Invalid service selected', 'booktics' ) ); } // An item may legitimately arrive without a team member when the // service assigns one server-side. The item validation rules decide // whether that is allowed; this check only verifies a member that was // actually supplied. if ( ! empty( $item['team_member_id'] ) ) { $team_member = ( new Team_Member_Model() )->find( $item['team_member_id'] ); if ( ! $team_member ) { $error->add( 'invalid_team_member', __( 'Invalid team member selected', 'booktics' ) ); } } $error = apply_filters( 'booktics_extra_service_validate', $error, $item ); return $error->has_errors() ? $error : true; } /** * Validate time slot availability * * @param array $item Order item data * * @return true|WP_Error Returns true if valid, WP_Error otherwise */ private function validate_time_slot( array $item ) { $error = new WP_Error(); $args = array( 'post_type' => 'booktics-appointment', 'post_status' => 'any', 'posts_per_page' => 1, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering appointments by meta fields 'meta_query' => array( 'relation' => 'AND', array( 'key' => 'date', 'value' => $item['date'], 'compare' => '=', ), array( 'key' => 'team_member_id', 'value' => $item['team_member_id'], 'compare' => '=', ), array( 'relation' => 'OR', // Appointment starts during requested time slot array( 'relation' => 'AND', array( 'key' => 'start_time', 'value' => $item['start_time'], 'compare' => '<=', ), array( 'key' => 'end_time', 'value' => $item['start_time'], 'compare' => '>', ), ), // Appointment ends during requested time slot array( 'relation' => 'AND', array( 'key' => 'start_time', 'value' => $item['end_time'], 'compare' => '<', ), array( 'key' => 'end_time', 'value' => $item['end_time'], 'compare' => '>=', ), ), ), ), ); $query = new \WP_Query( $args ); if ( $query->have_posts() ) { $error->add( 'time_slot_unavailable', sprintf( /* translators: 1: Start time, 2: End time, 3: Date */ __( 'Time slot %1$s - %2$s on %3$s is not available', 'booktics' ), $item['start_time'], $item['end_time'], $item['date'] ) ); } return $error->has_errors() ? $error : true; } }