# booktics/1.0.28/core/cart/controllers/cart-controller.php

Booktics – Appointment Booking Calendar for Service Businesses, version 1.0.28. 428 lines.

- Page: https://pluginprobe.com/plugins/booktics/1.0.28/code/core/cart/controllers/cart-controller.php
- Raw: https://pluginprobe.com/plugins/booktics/1.0.28/raw/core/cart/controllers/cart-controller.php
- Modified: 2026-10-05T09:02:00+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/booktics/1.0.28/code/core/cart/controllers/cart-controller.php#L10-L20`.

```php
<?php

namespace Booktics\Cart\Controllers;

use Booktics\Abstracts\Base_Rest_Controller;
use Booktics\Cart\Cart_Manager;
use Booktics\Models\Service_Model;
use Booktics\Models\Team_Member_Model;
use WP_Error;
use WP_REST_Response;
use WP_REST_Server;

class Cart_Controller extends Base_Rest_Controller {

    /**
     * Endpoint namespace
     *
     * @var string
     */
    protected $namespace = 'booktics/v1';

    /**
     * Route name
     *
     * @var string
     */
    protected $base = 'carts';

    /**
     * @var Cart_Manager
     */
    private $cart_manager;

    /**
     * Initialize CartController with a new CartManager instance
     */
    public function __construct() {
        $this->cart_manager = new Cart_Manager();
    }

    /**
     * Register REST API routes for cart functionality
     *
     * @return void
     */
    public function register_routes(): void {
        register_rest_route(
            $this->namespace,
            $this->base,
            array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => array( $this, 'add_to_cart' ),
                'permission_callback' => '__return_true',
            )
        );

        register_rest_route(
            $this->namespace,
            $this->base . '/(?P<uuid>[a-zA-Z0-9-]+)',
            array(
                'methods'             => WP_REST_Server::DELETABLE,
                'callback'            => array( $this, 'delete_from_cart' ),
                'permission_callback' => '__return_true',
            )
        );

        register_rest_route(
            $this->namespace,
            $this->base,
            array(
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => array( $this, 'sync_cart' ),
                'permission_callback' => array( $this, 'is_user_logged_in' ),
            )
        );

        register_rest_route(
            $this->namespace,
            $this->base . '/(?P<uuid>[a-zA-Z0-9-]+)',
            array(
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => array( $this, 'edit_cart_item' ),
                'permission_callback' => '__return_true',
            )
        );

        register_rest_route(
            $this->namespace,
            $this->base,
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_cart_items' ),
                'permission_callback' => '__return_true',
            )
        );
    }

    /**
     * Prepare and validate cart item data
     *
     * @param array $data
     *
     * @return []|array|bool|WP_Error
     */
    private function prepare_item_to_save( array $data ) {
        $item_rules = array(
            'date'           => array( 'required' ),
            'start_time'     => array( 'required' ),
            'service_id'     => array( 'required' ),
            'team_member_id' => array( 'required' ),
        );

        /**
         * Filters the validation rules applied to a cart item.
         *
         * Extensions that assign a team member server-side (Booktics Pro
         * round robin) drop the team_member_id requirement here.
         *
         * @since 1.0.26
         *
         * @param array $item_rules Validation rules.
         * @param array $data       The cart item being validated.
         */
        $item_rules = apply_filters( 'booktics_cart_item_validation_rules', $item_rules, $data );

        $validated = booktics_validate( $data, $item_rules );

        if ( is_wp_error( $validated ) ) {
            return $validated;
        }

        $item_validation = $this->validate_cart_item( $data );
        if ( is_wp_error( $item_validation ) ) {
            return $item_validation;
        }

        return $item_validation;
    }

    /**
     * Add an item to the cart
     *
     * @param $request
     *
     * @return array|\WP_HTTP_Response
     */
    public function add_to_cart( $request ) {
        $params    = json_decode( $request->get_body(), true );
        $validated = $this->prepare_item_to_save( $params );

        if ( is_wp_error( $validated ) ) {
            return $this->error(
                __( 'Cart validation error', 'booktics' ),
                422,
                'Validation',
                $validated->errors
            );
        }
        // Calculate end time from start time and total duration
        $total_duration_min  = $this->calculate_total_duration( $validated );
        $validated['end_time'] = $this->get_endtime_from_starttime($validated['start_time'], $total_duration_min);

        $response            = $this->cart_manager->add_to_cart( $validated );
        $response['service'] = $this->get_service( $response['service_id'] );
        $response['team']    = $this->get_team_member( $response['team_member_id'] );

        // Allow booktics-pro to add location details
        $response = apply_filters( 'booktics_cart_item_response', $response );

        return $this->response( $response, __( 'Item added to cart', 'booktics' ), 201 );
    }

    /**
     * Delete an item from the user's cart
     *
     * @param $request
     *
     * @return array|\WP_HTTP_Response
     */
    public function delete_from_cart( $request ) {
        $uuid = sanitize_text_field( $request->get_param( 'uuid' ) );

        if ( empty( $uuid ) ) {
            return $this->error(
                __( 'UUID is required', 'booktics' ),
                422,
                'Validation'
            );
        }

        $this->cart_manager->remove_cart_item( $uuid );

        return $this->response( array(), __( 'Successfully deleted item from cart.', 'booktics' ) );
    }

    /**
     * Synchronize cart session data to database for logged-in user
     *
     * @param $request
     *
     * @return array|\WP_HTTP_Response
     */
    public function sync_cart( $request ) {
        $user_id = get_current_user_id();
        $this->cart_manager->sync_session_to_db( $user_id );

        $response = $this->cart_manager->get_cart();

        return $this->response( $response, __( 'Successfully synced cart.', 'booktics' ) );
    }

    /**
     * Get all items from the cart
     *
     * @return array|\WP_HTTP_Response
     */
    public function get_cart_items() {
        $response          = $this->cart_manager->get_cart();
        $response['items'] = isset( $response['items'] ) ? array_map(
            function ( $item ) {
                if ( ! is_array( $item ) ) {
                        $item = (array) $item;
                }

                $item['service'] = $this->get_service( $item['service_id'] );
                $item['team']    = $this->get_team_member( $item['team_member_id'] );

                // Allow booktics-pro to add location details
                $item = apply_filters( 'booktics_cart_item_response', $item );

                return $item;
            }, $response['items']
        ) : array();

        return $this->response( $response, __( 'Cart items fetched successfully', 'booktics' ) );
    }

    /**
     * Check if the current user is logged in
     *
     * @return bool True if user is logged in, false otherwise
     */
    public function is_user_logged_in() {
        return is_user_logged_in();
    }

    /**
     * Validate service, team member and price
     *
     * @param array $data Cart item data
     *
     * @return array|WP_Error Returns true if valid, WP_Error otherwise
     */
    private function validate_cart_item( array $data ) {
        $error = new WP_Error();

        $service = ( new Service_Model() )->find( $data['service_id'] );
        if ( ! $service ) {
            $error->add( 'invalid_service', __( 'Invalid service selected', 'booktics' ) );
        }

        // A cart item may legitimately arrive without a team member when the
        // service assigns one server-side. Only verify a member that was
        // actually supplied; the booking is refused later if none can be found.
        if ( ! empty( $data['team_member_id'] ) ) {
            $team_member = ( new Team_Member_Model() )->find( $data['team_member_id'] );
            if ( empty( $team_member ) ) {
                $error->add( 'invalid_team_member', __( 'Invalid team member selected', 'booktics' ) );
            }
        }

        $service_price      = 0;
        $addition_durations = $service->additional_durations;
        if ( ! empty( $addition_durations ) && isset( $data['additional_duration_id'] ) ) {
            $addition_durations = array_filter(
                $addition_durations, function ( $duration ) use ( $data ) {
					return $duration['id'] === $data['additional_duration_id'];
				}
            );
            $addition_duration  = reset( $addition_durations );
            $service_price      = $addition_duration['charge_amount'] ?? 0;
        }
        $data['price'] = $service_price;

        return $error->has_errors() ? $error : $data;
    }

    /**
     * Edit a cart item
     *
     * @param $request
     *
     * @return array|\WP_HTTP_Response
     */
    public function edit_cart_item( $request ) {
        $uuid   = sanitize_text_field( (string) $request->get_param( 'uuid' ) );
        $params = json_decode( $request->get_body(), true );

        if ( empty( $uuid ) ) {
            return $this->error(
                __( 'UUID is required', 'booktics' ),
                422,
                'Validation'
            );
        }

        $validated = $this->prepare_item_to_save( $params );
        if ( is_wp_error( $validated ) ) {
            return $this->error(
                __( 'Cart validation error', 'booktics' ),
                422,
                'Validation',
                $validated->errors
            );
        }

        // Add UUID to validated data
        $validated['uuid'] = $uuid;

        // Calculate end time from start time and total duration
        $total_duration_min    = $this->calculate_total_duration( $validated );
        $validated['end_time'] = $this->get_endtime_from_starttime( $validated['start_time'], $total_duration_min );

        // Update the cart item
        $updated_item = $this->cart_manager->edit_cart_item( $uuid, $validated );

        if ( empty( $updated_item ) ) {
            return $this->error(
                __( 'Cart item not found', 'booktics' ),
                404,
                'NotFound'
            );
        }

        // Allow booktics-pro to add location details
        $updated_item = apply_filters( 'booktics_cart_item_response', $updated_item );

        return $this->response(
            $updated_item,
            __( 'Successfully updated cart item', 'booktics' )
        );
    }

    /**
     * @param $service_id
     *
     * @return array
     */
    public function get_service( $service_id ): array {
        $service = Service_Model::find( $service_id );
        if ( empty( $service ) ) {
            return array();
        }

        return array(
            'id'    => $service->id,
            'title' => $service->title,
        );
    }

    /**
     * @param $team_member_id
     *
     * @return array
     */
    public function get_team_member( $team_member_id ): array {
        $team_member = ( new Team_Member_Model() )->find( $team_member_id );
        if ( empty( $team_member ) ) {
            return array();
        }
        $team_member = $team_member->to_array();

        return array(
            'id'           => $team_member['id'],
            'display_name' => $team_member['display_name'] ?? '',
            'image'        => $team_member['image'] ?? '',
        );
    }

    /**
     * Calculate total duration for a cart item (appointment)
     *
     * @param array $item Cart item data
     *
     * @return int Total duration in minutes
     */
    private function calculate_total_duration( $item ) {
        $service_model = Service_Model::find( $item['service_id'] );
        if ( ! $service_model ) {
            return 0;
        }
        $service_duration = array_filter(
            $service_model->additional_durations, function ( $duration ) use ( $item ) {
				return $item['additional_duration_id'] === $duration['id'];
			}
        );
        if ( empty( $service_duration ) ) {
            return 0;
        }
        $service_duration   = reset( $service_duration );
        $total_duration_min = $service_duration['duration_unit'] === 'min'
            ? $service_duration['duration_time']
            : ( $service_duration['duration_time'] * 60 );
        if ( ! empty( $item['extra_services'] ) ) {
            $total_duration_min = apply_filters( 'booktics_extra_service_duration', $total_duration_min, $item['extra_services'] );
        }

        $total_duration_min = apply_filters( 'booktics_calculate_total_duration', $total_duration_min, $item );

        return $total_duration_min;
    }

    /**
     * Get end time for appointments from start time and total duration
     *
     * @param $start_time   start time of the appointment
     * @param $total_duration_min total duration of the appointment in minutes including service time and extra service time
     *
     * @return string end time of the appointment
     */
    private function get_endtime_from_starttime( $start_time, $total_duration_min ) {
        $start_time = strtotime( $start_time );
        $end_time   = strtotime( '+' . $total_duration_min . ' minutes', $start_time );

        return gmdate( 'H:i', $end_time );
    }
}

```
