Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/latepoint/blocks/bricks/bricks_widget_book_button.php
+++ b/latepoint/blocks/bricks/bricks_widget_book_button.php
@@ -373,28 +373,32 @@
// Render element HTML
public function render() {
- $allowed_params = [
- 'caption',
- 'hide_summary',
- 'hide_side_panel',
- 'selected_agent',
- 'selected_service',
- 'selected_bundle',
- 'selected_service_category',
- 'selected_location',
- 'selected_start_date',
- 'selected_start_time',
- 'selected_duration',
- 'selected_total_attendees',
- 'source_id',
- 'calendar_start_date',
- 'show_services',
- 'show_service_categories',
- 'show_agents',
- 'show_locations',
- 'btn_wrapper_classes',
- 'btn_classes'
- ];
+ $allowed_params = apply_filters(
+ 'latepoint_book_widget_allowed_params',
+ [
+ 'caption',
+ 'hide_summary',
+ 'hide_side_panel',
+ 'selected_agent',
+ 'selected_service',
+ 'selected_bundle',
+ 'selected_service_category',
+ 'selected_location',
+ 'selected_start_date',
+ 'selected_start_time',
+ 'selected_duration',
+ 'selected_total_attendees',
+ 'source_id',
+ 'calendar_start_date',
+ 'show_services',
+ 'show_service_categories',
+ 'show_agents',
+ 'show_locations',
+ 'btn_wrapper_classes',
+ 'btn_classes',
+ ],
+ 'bricks_book_button'
+ );
$this->settings['btn_wrapper_classes'] = 'bricks-button-wrapper';
$this->settings['btn_classes'] = 'bricks-button bricks-background-primary';
--- a/latepoint/blocks/bricks/bricks_widget_book_form.php
+++ b/latepoint/blocks/bricks/bricks_widget_book_form.php
@@ -220,24 +220,28 @@
// Render element HTML
public function render() {
- $allowed_params = [
- 'hide_summary',
- 'hide_side_panel',
- 'selected_agent',
- 'selected_service',
- 'selected_service_category',
- 'selected_location',
- 'selected_start_date',
- 'selected_start_time',
- 'selected_duration',
- 'selected_total_attendees',
- 'source_id',
- 'calendar_start_date',
- 'show_services',
- 'show_service_categories',
- 'show_agents',
- 'show_locations',
- ];
+ $allowed_params = apply_filters(
+ 'latepoint_book_widget_allowed_params',
+ [
+ 'hide_summary',
+ 'hide_side_panel',
+ 'selected_agent',
+ 'selected_service',
+ 'selected_service_category',
+ 'selected_location',
+ 'selected_start_date',
+ 'selected_start_time',
+ 'selected_duration',
+ 'selected_total_attendees',
+ 'source_id',
+ 'calendar_start_date',
+ 'show_services',
+ 'show_service_categories',
+ 'show_agents',
+ 'show_locations',
+ ],
+ 'bricks_book_form'
+ );
$params = OsBlockHelper::attributes_to_data_params($this->settings, $allowed_params);
$output = "<div {$this->render_attributes( '_root' )}>";
--- a/latepoint/blocks/elementor/elementor_widget_book_button.php
+++ b/latepoint/blocks/elementor/elementor_widget_book_button.php
@@ -508,28 +508,32 @@
protected function render(): void {
$settings = $this->get_settings_for_display();
- $allowed_params = [
- 'caption',
- 'hide_summary',
- 'hide_side_panel',
- 'selected_agent',
- 'selected_service',
- 'selected_bundle',
- 'selected_service_category',
- 'selected_location',
- 'selected_start_date',
- 'selected_start_time',
- 'selected_duration',
- 'selected_total_attendees',
- 'source_id',
- 'calendar_start_date',
- 'show_services',
- 'show_service_categories',
- 'show_agents',
- 'show_locations',
- 'btn_wrapper_classes',
- 'btn_classes'
- ];
+ $allowed_params = apply_filters(
+ 'latepoint_book_widget_allowed_params',
+ [
+ 'caption',
+ 'hide_summary',
+ 'hide_side_panel',
+ 'selected_agent',
+ 'selected_service',
+ 'selected_bundle',
+ 'selected_service_category',
+ 'selected_location',
+ 'selected_start_date',
+ 'selected_start_time',
+ 'selected_duration',
+ 'selected_total_attendees',
+ 'source_id',
+ 'calendar_start_date',
+ 'show_services',
+ 'show_service_categories',
+ 'show_agents',
+ 'show_locations',
+ 'btn_wrapper_classes',
+ 'btn_classes',
+ ],
+ 'elementor_book_button'
+ );
$settings['btn_wrapper_classes'] = 'elementor-button-wrapper';
$settings['btn_classes'] = 'elementor-button elementor-button-link elementor-size-sm';
--- a/latepoint/blocks/elementor/elementor_widget_book_form.php
+++ b/latepoint/blocks/elementor/elementor_widget_book_form.php
@@ -273,24 +273,28 @@
protected function render(): void {
$settings = $this->get_settings_for_display();
- $allowed_params = [
- 'hide_summary',
- 'hide_side_panel',
- 'selected_agent',
- 'selected_service',
- 'selected_service_category',
- 'selected_location',
- 'selected_start_date',
- 'selected_start_time',
- 'selected_duration',
- 'selected_total_attendees',
- 'source_id',
- 'calendar_start_date',
- 'show_services',
- 'show_service_categories',
- 'show_agents',
- 'show_locations',
- ];
+ $allowed_params = apply_filters(
+ 'latepoint_book_widget_allowed_params',
+ [
+ 'hide_summary',
+ 'hide_side_panel',
+ 'selected_agent',
+ 'selected_service',
+ 'selected_service_category',
+ 'selected_location',
+ 'selected_start_date',
+ 'selected_start_time',
+ 'selected_duration',
+ 'selected_total_attendees',
+ 'source_id',
+ 'calendar_start_date',
+ 'show_services',
+ 'show_service_categories',
+ 'show_agents',
+ 'show_locations',
+ ],
+ 'elementor_book_form'
+ );
$params = OsBlockHelper::attributes_to_data_params($settings, $allowed_params);
echo do_shortcode('[latepoint_book_form ' . $params . ']');
--- a/latepoint/latepoint.php
+++ b/latepoint/latepoint.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: LatePoint
* Description: Appointment Scheduling Software for WordPress
- * Version: 5.6.1
+ * Version: 5.6.2
* Author: LatePoint
* Author URI: https://latepoint.com
* Plugin URI: https://latepoint.com
@@ -29,7 +29,7 @@
* LatePoint version.
*
*/
- public $version = '5.6.1';
+ public $version = '5.6.2';
public $db_version = '2.3.0';
@@ -114,6 +114,9 @@
if ( ! defined( 'LATEPOINT_VERSION' ) ) {
define( 'LATEPOINT_VERSION', $this->version );
}
+ if ( ! defined( 'LATEPOINT_BRAND_NAME' ) ) {
+ define( 'LATEPOINT_BRAND_NAME', 'LatePoint' );
+ }
if ( ! defined( 'LATEPOINT_MIN_REQUIRED_PRO_VERSION' ) ) {
define( 'LATEPOINT_MIN_REQUIRED_PRO_VERSION', '1.2.5' );
}
@@ -1362,12 +1365,12 @@
function init_menus() {
// link for admins
add_menu_page(
- __( 'LatePoint', 'latepoint' ),
- __( 'LatePoint', 'latepoint' ),
+ OsSettingsHelper::get_brand_name(),
+ OsSettingsHelper::get_brand_name(),
OsAuthHelper::get_current_user()->wp_capability,
'latepoint',
[ $this, 'route_call' ],
- 'none'
+ apply_filters( 'latepoint_admin_menu_icon', 'none' )
);
}
@@ -1375,9 +1378,10 @@
function add_latepoint_link_to_admin_bar( $wp_admin_bar ) {
if ( OsAuthHelper::get_current_user()->has_backend_access() ) {
// build link depending on who is logged in
- $args = [
+ $admin_bar_label = OsSettingsHelper::get_brand_name();
+ $args = [
'id' => 'latepoint_top_link',
- 'title' => '<span class="latepoint-icon latepoint-icon-lp-logo" style="margin-right: 7px;"></span><span style="">' . __( 'LatePoint', 'latepoint' ) . '</span>',
+ 'title' => '<span class="latepoint-icon latepoint-icon-lp-logo" style="margin-right: 7px;"></span><span style="">' . esc_html( $admin_bar_label ) . '</span>',
'href' => OsRouterHelper::build_link( [ 'dashboard', 'index' ] ),
'meta' => array( 'class' => '' ),
];
@@ -1646,9 +1650,11 @@
wp_localize_script( 'latepoint-main-admin', 'latepoint_helper', $localized_vars );
+ // Single shared localize for the delete-confirm modal — used by BOTH the appointments bulk
+ // delete and every single-record delete (services now, others later). Single deletes show the
wp_localize_script(
'latepoint-main-admin',
- 'latepoint_bookings_bulk_i18n',
+ 'latepoint_delete_confirm_i18n',
array(
'modal_title' => __( 'Are you sure you want to delete these appointments?', 'latepoint' ),
'modal_body_one' => __( 'You are about to delete 1 appointment. This action cannot be undone.', 'latepoint' ),
@@ -1664,7 +1670,6 @@
)
);
-
$latepoint_css_variables = OsStylesHelper::generate_css_variables();
wp_add_inline_style( 'latepoint-main-admin', $latepoint_css_variables );
}
--- a/latepoint/lib/abilities/class-latepoint-abilities.php
+++ b/latepoint/lib/abilities/class-latepoint-abilities.php
@@ -61,7 +61,7 @@
wp_register_ability_category(
'latepoint',
[
- 'label' => __( 'LatePoint', 'latepoint' ),
+ 'label' => OsSettingsHelper::get_brand_name(),
'description' => __( 'Appointment scheduling — bookings, customers, services, and staff.', 'latepoint' ),
]
);
--- a/latepoint/lib/controllers/bookings_controller.php
+++ b/latepoint/lib/controllers/bookings_controller.php
@@ -259,6 +259,8 @@
$this->vars['records_ordered_by_key'] = $order_by['key'];
$this->vars['records_ordered_by_direction'] = $order_by['direction'];
+ $this->vars['can_bulk_delete'] = OsRolesHelper::can_user_perform_model_action( 'OsBookingModel', 'delete' );
+
// OUTPUT CSV IF REQUESTED
if ( isset( $this->params['download'] ) && $this->params['download'] == 'csv' ) {
$csv_filename = 'all_bookings_' . OsUtilHelper::random_text() . '.csv';
--- a/latepoint/lib/controllers/orders_controller.php
+++ b/latepoint/lib/controllers/orders_controller.php
@@ -52,10 +52,13 @@
$order_intent->convert_to_order();
- if ( $order_intent ) {
- nocache_headers();
- wp_redirect( $order_intent->get_page_url_with_intent(), 302 );
- }
+ nocache_headers();
+ // Redirect with the intent key in either outcome, front.js resumes the booking flow at the confirmation
+ // step when convert_to_order succeeded, or at the payment step when it didn't (e.g. cancelled Mollie/PayPal
+ // payment). The "double payment information" issue is fixed at the cart-model layer (see
+ // OsCartModel::clear) without that fix, the resume lightbox showed every cart item twice.
+ wp_redirect( $order_intent->get_page_url_with_intent(), 302 );
+ exit;
}
}
@@ -169,7 +172,7 @@
array(
'status' => LATEPOINT_STATUS_ERROR,
'message' => sprintf( __( 'Error: %s', 'latepoint' ), implode( ', ', $validation_errors ) ),
- )
+ )
);
}
@@ -412,7 +415,7 @@
array(
'status' => $status,
'message' => $response_html,
- )
+ )
);
}
}
@@ -488,7 +491,7 @@
array(
'status' => LATEPOINT_STATUS_SUCCESS,
'message' => $response_html,
- )
+ )
);
}
}
@@ -514,7 +517,7 @@
array(
'status' => LATEPOINT_STATUS_SUCCESS,
'message' => $response_html,
- )
+ )
);
}
}
@@ -536,7 +539,7 @@
array(
'status' => LATEPOINT_STATUS_SUCCESS,
'message' => $response_html,
- )
+ )
);
}
@@ -567,7 +570,7 @@
array(
'status' => LATEPOINT_STATUS_SUCCESS,
'message' => $response_html,
- )
+ )
);
}
@@ -692,7 +695,7 @@
array(
'status' => $status,
'message' => $response_html,
- )
+ )
);
}
}
@@ -793,7 +796,7 @@
];
$values_row = apply_filters( 'latepoint_order_row_for_csv_export', $values_row, $order, $this->params );
$orders_data[] = $values_row;
- }
+ }
}
$orders_data = apply_filters( 'latepoint_orders_data_for_csv_export', $orders_data, $this->params );
@@ -838,7 +841,7 @@
'showing_from' => $this->vars['showing_from'],
'showing_to' => $this->vars['showing_to'],
'total_records' => $total_orders,
- ]
+ ]
);
}
}
--- a/latepoint/lib/controllers/pro_controller.php
+++ b/latepoint/lib/controllers/pro_controller.php
@@ -68,6 +68,12 @@
$this->vars['page_header'] = __( 'Assets', 'latepoint' );
$this->format_render( 'pro_feature', [], [], true );
}
+
+ public function white_label() {
+ $this->vars['page_header'] = OsMenuHelper::get_menu_items_by_id( 'settings' );
+ $this->vars['pre_page_header'] = OsMenuHelper::get_label_by_id( 'settings' );
+ $this->format_render( 'pro_feature', [], [], true );
+ }
}
endif;
--- a/latepoint/lib/controllers/steps_controller.php
+++ b/latepoint/lib/controllers/steps_controller.php
@@ -79,6 +79,7 @@
$calendar_settings['earliest_possible_booking'] = OsSettingsHelper::get_earliest_possible_booking_restriction( OsStepsHelper::$booking_object->service_id ?? false );
$calendar_settings['latest_possible_booking'] = OsSettingsHelper::get_latest_possible_booking_restriction( OsStepsHelper::$booking_object->service_id ?? false );
+ $calendar_settings['consider_cart_items'] = true;
$this->format_render(
'partials/_monthly_calendar_days',
--- a/latepoint/lib/helpers/blocks_helper.php
+++ b/latepoint/lib/helpers/blocks_helper.php
@@ -140,7 +140,7 @@
// Adding a new category.
$categories[] = [
'slug' => 'latepoint',
- 'title' => 'LatePoint',
+ 'title' => OsSettingsHelper::get_brand_name(),
];
return $categories;
}
--- a/latepoint/lib/helpers/booking_helper.php
+++ b/latepoint/lib/helpers/booking_helper.php
@@ -658,64 +658,83 @@
}
- public static function generate_bundles_folder(): void {
- $bundles_model = new OsBundleModel();
- $bundles = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();
+ public static function generate_bundles_folder( ?array $bundles = null, array $restrictions = [] ): void {
+ if ( null === $bundles ) {
+ $bundles_model = new OsBundleModel();
+ $bundles = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();
+ }
if ( $bundles ) {
- ?>
- <div class="os-item-category-w os-items os-as-rows os-animated-child">
- <div class="os-item-category-info-w os-item os-animated-self with-plus">
- <div class="os-item-category-info os-item-i" tabindex="0">
- <div class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></div>
- <div class="os-item-name-w">
- <div class="os-item-name"><?php echo esc_html__( 'Bundle & Save', 'latepoint' ); ?></div>
- </div>
- <?php if ( OsSettingsHelper::is_on( 'show_service_categories_count' ) && count( $bundles ) ) { ?>
- <div class="os-item-child-count">
- <span><?php echo count( $bundles ); ?></span> <?php esc_html_e( 'Bundles', 'latepoint' ); ?>
+ /**
+ * Whether to skip the "Bundle & Save" folder header and render bundle
+ * tiles directly (without a collapsible category wrapper).
+ *
+ * @param bool $skip Whether to skip the folder wrapper. Default false.
+ * @param array $restrictions Active booking-form restrictions.
+ *
+ * @hook latepoint_bundles_folder_skip_wrapper
+ */
+ $skip_folder_wrapper = (bool) apply_filters( 'latepoint_bundles_folder_skip_wrapper', false, $restrictions );
+
+ if ( ! $skip_folder_wrapper ) {
+ ?>
+ <div class="os-item-category-w os-items os-as-rows os-animated-child">
+ <div class="os-item-category-info-w os-item os-animated-self with-plus">
+ <div class="os-item-category-info os-item-i" tabindex="0">
+ <div class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></div>
+ <div class="os-item-name-w">
+ <div class="os-item-name"><?php echo esc_html__( 'Bundle & Save', 'latepoint' ); ?></div>
</div>
- <?php } ?>
+ <?php if ( OsSettingsHelper::is_on( 'show_service_categories_count' ) && count( $bundles ) ) { ?>
+ <div class="os-item-child-count">
+ <span><?php echo count( $bundles ); ?></span> <?php esc_html_e( 'Bundles', 'latepoint' ); ?>
+ </div>
+ <?php } ?>
+ </div>
</div>
- </div>
- <div class="os-bundles os-animated-parent os-items os-as-rows os-selectable-items">
- <?php
- foreach ( $bundles as $bundle ) { ?>
- <div class="os-animated-child os-item os-selectable-item <?php echo ( $bundle->charge_amount ) ? 'os-priced-item' : ''; ?> <?php if ( $bundle->short_description ) {
- echo 'with-description'; } ?>"
- tabindex="0"
- data-item-price="<?php echo esc_attr( $bundle->charge_amount ); ?>"
- data-priced-item-type="bundle"
- data-summary-field-name="bundle"
- data-summary-value="<?php echo esc_attr( $bundle->name ); ?>"
- data-item-id="<?php echo esc_attr( $bundle->id ); ?>"
- data-cart-item-item-data-key="bundle_id"
- data-os-call-func="latepoint_bundle_selected">
- <div class="os-service-selector os-item-i os-animated-self"
- data-bundle-id="<?php echo esc_attr( $bundle->id ); ?>">
- <span class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></span>
- <span class="os-item-name-w">
- <span class="os-item-name"><?php echo esc_html( $bundle->name ); ?></span>
- <?php if ( $bundle->short_description ) { ?>
- <span class="os-item-desc"><?php echo wp_kses_post( $bundle->short_description ); ?></span>
- <?php } ?>
- </span>
+ <?php
+ }
+ ?>
+ <div class="os-bundles os-animated-parent os-items os-as-rows os-selectable-items">
+ <?php
+ foreach ( $bundles as $bundle ) { ?>
+ <div class="os-animated-child os-item os-selectable-item <?php echo ( $bundle->charge_amount ) ? 'os-priced-item' : ''; ?> <?php if ( $bundle->short_description ) {
+ echo 'with-description'; } ?>"
+ tabindex="0"
+ data-item-price="<?php echo esc_attr( $bundle->charge_amount ); ?>"
+ data-priced-item-type="bundle"
+ data-summary-field-name="bundle"
+ data-summary-value="<?php echo esc_attr( $bundle->name ); ?>"
+ data-item-id="<?php echo esc_attr( $bundle->id ); ?>"
+ data-cart-item-item-data-key="bundle_id"
+ data-os-call-func="latepoint_bundle_selected">
+ <div class="os-service-selector os-item-i os-animated-self"
+ data-bundle-id="<?php echo esc_attr( $bundle->id ); ?>">
+ <span class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></span>
+ <span class="os-item-name-w">
+ <span class="os-item-name"><?php echo esc_html( $bundle->name ); ?></span>
+ <?php if ( $bundle->short_description ) { ?>
+ <span class="os-item-desc"><?php echo wp_kses_post( $bundle->short_description ); ?></span>
+ <?php } ?>
+ </span>
- <?php if ( $bundle->charge_amount > 0 ) { ?>
- <span class="os-item-price-w">
+ <?php if ( $bundle->charge_amount > 0 ) { ?>
+ <span class="os-item-price-w">
<span class="os-item-price">
- <?php echo esc_html( OsMoneyHelper::format_price( $bundle->charge_amount ) ); ?>
+ <?php echo esc_html( OsMoneyHelper::format_price( $bundle->charge_amount ) ); ?>
</span>
</span>
- <?php } ?>
- </div>
+ <?php } ?>
</div>
- <?php
- }
- ?>
- </div>
+ </div>
+ <?php
+ }
+ ?>
</div>
<?php
+ if ( ! $skip_folder_wrapper ) {
+ echo '</div>';
+ }
}
}
@@ -787,6 +806,8 @@
'show_services_arr' => false,
'preselected_service' => false,
'preselected_category' => false,
+ 'bundles' => null,
+ 'service_display_mode' => 'all',
];
$settings = array_merge( $default_settings, $settings );
@@ -840,6 +861,8 @@
$services_without_category->where_in( 'id', $settings['show_services_arr'] );
}
$services_without_category = $services_without_category->where( [ 'category_id' => 0 ] )->should_be_active()->should_not_be_hidden()->get_results_as_models();
+ /** This filter is documented in lib/helpers/steps_helper.php */
+ $services_without_category = apply_filters( 'latepoint_step_services', $services_without_category, $settings['bundles'], [ 'service_display_mode' => $settings['service_display_mode'] ] );
if ( $services_without_category ) {
OsBookingHelper::generate_services_list( $services_without_category, false );
}
@@ -862,6 +885,8 @@
$services = $category_services;
}
}
+ /** This filter is documented in lib/helpers/steps_helper.php */
+ $services = apply_filters( 'latepoint_step_services', $services, $settings['bundles'], [ 'service_display_mode' => $settings['service_display_mode'] ] );
$child_categories = new OsServiceCategoryModel();
$count_child_categories = $child_categories->where( [ 'parent_id' => $service_category->id ] )->count();
// show only if it has either at least one child category or service
@@ -898,7 +923,14 @@
}
}
if ( ! $settings['preselected_category'] && ! $parent_id ) {
- OsBookingHelper::generate_bundles_folder();
+ $category_bundles = $settings['bundles'];
+ if ( null === $category_bundles ) {
+ $bundles_model = new OsBundleModel();
+ $category_bundles = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();
+ /** This filter is documented in lib/helpers/steps_helper.php */
+ $category_bundles = apply_filters( 'latepoint_step_bundles', $category_bundles, [], [ 'service_display_mode' => $settings['service_display_mode'] ] );
+ }
+ OsBookingHelper::generate_bundles_folder( $category_bundles, [ 'service_display_mode' => $settings['service_display_mode'] ] );
}
if ( ! $settings['preselected_category'] ) {
echo '</div>';
--- a/latepoint/lib/helpers/calendar_helper.php
+++ b/latepoint/lib/helpers/calendar_helper.php
@@ -15,6 +15,9 @@
<?php if ( $auto_search ) { ?>
<div class="os-calendar-searching-info"><?php echo sprintf( esc_html__( 'Searching %s for available dates', 'latepoint' ), '<span></span>' ); ?></div>
<?php } ?>
+ <?php $no_availability_message = apply_filters( 'latepoint_no_availability_message', __( 'No appointments are currently available. Please check back later or contact us for assistance.', 'latepoint' ) );
+ ?>
+ <div class="os-no-availability-message latepoint-message latepoint-message-info" style="display: none;"><?php echo esc_html( $no_availability_message ); ?></div>
<div class="os-calendar-while-searching-wrapper">
<?php OsCalendarHelper::generate_calendar_for_datepicker_step( LatePointMiscBookingRequest::create_from_booking_model( $booking ), $target_date, $calendar_settings ); ?>
</div>
--- a/latepoint/lib/helpers/invoices_helper.php
+++ b/latepoint/lib/helpers/invoices_helper.php
@@ -134,6 +134,7 @@
public static function invoice_document_html( OsInvoiceModel $invoice, bool $show_controls, ?OsTransactionModel $transaction = null ) {
$invoice_data = json_decode( $invoice->data, true );
+ $amount_due = empty( $transaction ) ? $invoice->get_amount_due() : 0;
?>
<div class="invoice-document status-<?php echo esc_attr( $invoice->status ); ?>" data-invoice-id="<?php echo esc_attr( $invoice->id ); ?>" data-route="<?php echo OsRouterHelper::build_route_name( 'invoices', 'change_status' ); ?>">
<?php if ( $show_controls ) { ?>
@@ -175,18 +176,16 @@
<?php } ?>
<div class="invoice-document-i">
<?php
- if ( empty( $transaction ) ) {
- switch ( $invoice->status ) {
- case LATEPOINT_INVOICE_STATUS_PAID:
- echo '<div class="invoice-status-paid-label">' . esc_html( self::readable_status( $invoice->status ) ) . '</div>';
- break;
- case LATEPOINT_INVOICE_STATUS_DRAFT:
- echo '<div class="invoice-status-draft-label">' . esc_html( self::readable_status( $invoice->status ) ) . '</div>';
- break;
- case LATEPOINT_INVOICE_STATUS_VOID:
- echo '<div class="invoice-status-voided-label">' . esc_html( self::readable_status( $invoice->status ) ) . '</div>';
- break;
- }
+ switch ( $invoice->status ) {
+ case LATEPOINT_INVOICE_STATUS_PAID:
+ echo '<div class="invoice-status-paid-label">' . esc_html( self::readable_status( $invoice->status ) ) . '</div>';
+ break;
+ case LATEPOINT_INVOICE_STATUS_DRAFT:
+ echo '<div class="invoice-status-draft-label">' . esc_html( self::readable_status( $invoice->status ) ) . '</div>';
+ break;
+ case LATEPOINT_INVOICE_STATUS_VOID:
+ echo '<div class="invoice-status-voided-label">' . esc_html( self::readable_status( $invoice->status ) ) . '</div>';
+ break;
}
?>
<div class="invoice-heading">
@@ -250,7 +249,7 @@
<?php if ( empty( $transaction ) ) { ?>
<div class="invoice-due-info">
<div class="invoice-due-amount">
- <?php echo esc_html( sprintf( __( '%s due %s', 'latepoint' ), OsMoneyHelper::format_price( $invoice->charge_amount, true, false ), OsTimeHelper::get_readable_date_from_string( $invoice->due_at ) ) ); ?>
+ <?php echo esc_html( sprintf( __( '%s due %s', 'latepoint' ), OsMoneyHelper::format_price( $amount_due, true, false ), OsTimeHelper::get_readable_date_from_string( $invoice->due_at ) ) ); ?>
</div>
<?php if ( $invoice->status == LATEPOINT_INVOICE_STATUS_OPEN ) { ?>
<div class="invoice-due-pay-link-w">
@@ -297,7 +296,7 @@
<?php } ?>
<div class="it-row it-row-bold">
<div class="it-column"><?php esc_html_e( 'Amount Due', 'latepoint' ); ?></div>
- <div class="it-column"><?php echo esc_html( OsMoneyHelper::format_price( $invoice->charge_amount, true, false ) ); ?></div>
+ <div class="it-column"><?php echo esc_html( OsMoneyHelper::format_price( $amount_due, true, false ) ); ?></div>
</div>
<?php } else { ?>
<div class="it-row it-row-bold">
--- a/latepoint/lib/helpers/menu_helper.php
+++ b/latepoint/lib/helpers/menu_helper.php
@@ -195,6 +195,12 @@
'icon' => '',
'link' => OsRouterHelper::build_link( [ 'pro', 'roles' ] ),
),
+ array(
+ 'id' => 'white_label',
+ 'label' => __( 'White Label', 'latepoint' ),
+ 'icon' => '',
+ 'link' => OsRouterHelper::build_link( [ 'pro', 'white_label' ] ),
+ ),
),
),
array(
--- a/latepoint/lib/helpers/roles_helper.php
+++ b/latepoint/lib/helpers/roles_helper.php
@@ -270,7 +270,7 @@
public static function register_customer_role() {
// Register Customer Role
- add_role( LATEPOINT_WP_CUSTOMER_ROLE, __( 'LatePoint Customer', 'latepoint' ), array( 'read' => true ) );
+ add_role( LATEPOINT_WP_CUSTOMER_ROLE, apply_filters( 'latepoint_role_display_name', __( 'LatePoint Customer', 'latepoint' ), LATEPOINT_USER_TYPE_CUSTOMER ), array( 'read' => true ) );
}
public static function register_roles_in_wp() {
--- a/latepoint/lib/helpers/settings_helper.php
+++ b/latepoint/lib/helpers/settings_helper.php
@@ -54,6 +54,10 @@
'currency_symbol_before' => '$',
];
+ public static function get_brand_name(): string {
+ return apply_filters( 'latepoint_brand_name', LATEPOINT_BRAND_NAME );
+ }
+
public static function get_business_logo_url() {
$default_logo_url = LATEPOINT_IMAGES_URL . 'logo.png';
@@ -883,7 +887,7 @@
'os-col-12' => __( 'Full Width', 'latepoint' ),
'os-col-6' => __( 'Half Width', 'latepoint' ),
),
- $default_field['width']
+ $default_field['width']
); ?>
</div>
</div>
@@ -953,4 +957,4 @@
}
}
-?>
No newline at end of file
+?>
--- a/latepoint/lib/helpers/shortcodes_helper.php
+++ b/latepoint/lib/helpers/shortcodes_helper.php
@@ -427,25 +427,29 @@
* @return false[]
*/
private static function get_default_booking_atts(): array {
- return [
- 'show_locations' => false,
- 'show_agents' => false,
- 'show_services' => false,
- 'show_service_categories' => false,
- 'selected_location' => false,
- 'selected_agent' => false,
- 'selected_service' => false,
- 'selected_duration' => false,
- 'selected_total_attendees' => false,
- 'selected_service_category' => false,
- 'selected_bundle' => false,
- 'calendar_start_date' => false,
- 'selected_start_date' => false,
- 'selected_start_time' => false,
- 'hide_side_panel' => false,
- 'hide_summary' => false,
- 'source_id' => false,
- ];
+ return apply_filters(
+ 'latepoint_default_booking_atts',
+ [
+ 'show_locations' => false,
+ 'show_agents' => false,
+ 'show_services' => false,
+ 'show_service_categories' => false,
+ 'selected_location' => false,
+ 'selected_agent' => false,
+ 'selected_service' => false,
+ 'selected_duration' => false,
+ 'selected_total_attendees' => false,
+ 'selected_service_category' => false,
+ 'selected_bundle' => false,
+ 'calendar_start_date' => false,
+ 'selected_start_date' => false,
+ 'selected_start_time' => false,
+ 'hide_side_panel' => false,
+ 'hide_summary' => false,
+ 'source_id' => false,
+ 'service_display_mode' => false,
+ ]
+ );
}
private static function generate_data_atts_string_from_atts( array $atts ): string {
--- a/latepoint/lib/helpers/steps_helper.php
+++ b/latepoint/lib/helpers/steps_helper.php
@@ -1136,7 +1136,8 @@
* @hook latepoint_set_restrictions
*
*/
- return apply_filters( 'latepoint_set_restrictions', self::$restrictions, $restrictions );
+ self::$restrictions = apply_filters( 'latepoint_set_restrictions', self::$restrictions, $restrictions );
+ return self::$restrictions;
}
/**
@@ -1735,6 +1736,30 @@
$services = $services_model->should_be_active()->should_not_be_hidden()->order_by( 'order_number asc' )->get_results_as_models();
+ /**
+ * Filter the list of services shown in the services selection step.
+ *
+ * @param OsServiceModel[] $services The services to display.
+ * @param OsBundleModel[] $bundles The bundles to display (unfiltered at this point).
+ * @param array $restrictions The active booking-form restrictions.
+ *
+ * @since 5.1.0
+ * @hook latepoint_step_services
+ */
+ $services = apply_filters( 'latepoint_step_services', $services, $bundles, self::$restrictions );
+
+ /**
+ * Filter the list of bundles shown in the services selection step.
+ *
+ * @param OsBundleModel[] $bundles The bundles to display.
+ * @param OsServiceModel[] $services The services to display (already filtered).
+ * @param array $restrictions The active booking-form restrictions.
+ *
+ * @since 5.1.0
+ * @hook latepoint_step_bundles
+ */
+ $bundles = apply_filters( 'latepoint_step_bundles', $bundles, $services, self::$restrictions );
+
self::$vars_for_view['show_services_arr'] = $show_services_arr;
self::$vars_for_view['show_service_categories_arr'] = $show_service_categories_arr;
self::$vars_for_view['preselected_category'] = $preselected_category;
@@ -1961,13 +1986,16 @@
if ( $customer ) {
$is_new_customer = false;
$old_customer_data = $customer->get_data_vars();
- // When merging by phone and the existing customer has a linked WP user,
- // do not allow an unauthenticated request to overwrite their identity fields.
- // Protects email (account takeover via password reset) and name fields (record integrity).
- if ( 'phone' === $contact_merge && ! empty( $customer->wordpress_user_id ) && ! is_user_logged_in() ) {
- unset( $sanitized_customer_params['email'] );
+ // In the auth-disabled (guest) flow anyone who knows a
+ // customer's merge contact value can submit the booking form and would
+ // otherwise overwrite that customer's PII. Strip identity fields so the
+ // booking still attaches to the existing record without mutating it.
+ if ( ! is_user_logged_in() ) {
unset( $sanitized_customer_params['first_name'] );
unset( $sanitized_customer_params['last_name'] );
+ unset( $sanitized_customer_params['email'] );
+ unset( $sanitized_customer_params['phone'] );
+ unset( $sanitized_customer_params['notes'] );
}
} else {
$is_new_customer = true;
--- a/latepoint/lib/misc/role.php
+++ b/latepoint/lib/misc/role.php
@@ -24,7 +24,7 @@
$this->wp_role = LATEPOINT_WP_ADMIN_ROLE;
break;
case LATEPOINT_USER_TYPE_AGENT:
- $this->name = __( 'LatePoint Agent', 'latepoint' );
+ $this->name = apply_filters( 'latepoint_role_display_name', __( 'LatePoint Agent', 'latepoint' ), LATEPOINT_USER_TYPE_AGENT );
$this->wp_role = LATEPOINT_WP_AGENT_ROLE;
break;
case LATEPOINT_USER_TYPE_CUSTOM:
@@ -117,7 +117,7 @@
$display_name = __( 'Administrator', 'latepoint' );
break;
case LATEPOINT_USER_TYPE_AGENT:
- $display_name = __( 'LatePoint Agent', 'latepoint' );
+ $display_name = apply_filters( 'latepoint_role_display_name', __( 'LatePoint Agent', 'latepoint' ), LATEPOINT_USER_TYPE_AGENT );
break;
case LATEPOINT_USER_TYPE_CUSTOM:
$display_name = $this->name;
--- a/latepoint/lib/models/cart_model.php
+++ b/latepoint/lib/models/cart_model.php
@@ -214,7 +214,10 @@
foreach ( $this->get_items() as $cart_item ) {
$cart_item->delete();
}
- unset( $this->items ); // important to unset, to avoid db queries
+ // Reset the in-memory cache to an empty array instead of unsetting it. If we unset, the next add_item() triggers
+ // get_items() to re-query the DB (which now contains the row just saved by add_item), and the caller then
+ // appends the same item again in memory, producing N+1 items.
+ $this->items = [];
}
/** ?
--- a/latepoint/lib/models/invoice_model.php
+++ b/latepoint/lib/models/invoice_model.php
@@ -93,12 +93,25 @@
[
'status' => LATEPOINT_TRANSACTION_STATUS_SUCCEEDED,
'invoice_id' => $this->id,
- ]
+ ]
)->get_results_as_models();
return $transactions;
}
+ public function get_amount_paid(): float {
+ $total = 0;
+ foreach ( $this->get_successful_payments() as $transaction ) {
+ $total += (float) $transaction->amount;
+ }
+
+ return $total;
+ }
+
+ public function get_amount_due(): float {
+ return (float) $this->charge_amount - $this->get_amount_paid();
+ }
+
protected function params_to_sanitize() {
return [
--- a/latepoint/lib/views/bookings/_table_body.php
+++ b/latepoint/lib/views/bookings/_table_body.php
@@ -5,6 +5,7 @@
/* @var $agents_list array */
/* @var $selected_columns array */
/* @var $ordered_columns array */
+/* @var $can_bulk_delete bool */
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
--- a/latepoint/lib/views/bookings/index.php
+++ b/latepoint/lib/views/bookings/index.php
@@ -18,14 +18,12 @@
/* @var $locations_list array */
/* @var $selected_columns array */
/* @var $ordered_columns array */
+/* @var $can_bulk_delete bool */
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
?>
-<?php
-$can_bulk_delete = OsRolesHelper::can_user_perform_model_action( 'OsBookingModel', 'delete' );
-?>
<?php if($bookings){ ?>
<div class="table-with-pagination-w has-scrollable-table">
<div class="os-pagination-w with-actions">
--- a/latepoint/lib/views/partials/_side_menu.php
+++ b/latepoint/lib/views/partials/_side_menu.php
@@ -6,7 +6,7 @@
<div class="latepoint-side-menu-w side-menu-<?php echo OsSettingsHelper::get_menu_layout_style(); ?>">
<div class="side-menu-top-part-w">
<a href="<?php echo esc_url(OsRouterHelper::build_link(['dashboard', 'index'])); ?>" class="logo-w">
- <img src="<?php echo esc_attr(LATEPOINT_IMAGES_URL . 'logo.svg'); ?>" width="20" height="20" alt="LatePoint Dashboard">
+ <img src="<?php echo esc_url( apply_filters( 'latepoint_admin_side_menu_logo_url', LATEPOINT_IMAGES_URL . 'logo.svg' ) ); ?>" width="20" height="20" alt="<?php echo esc_attr( __( 'Dashboard', 'latepoint' ) ); ?>">
</a>
<a href="#" data-route="<?php echo esc_attr(OsRouterHelper::build_route_name('settings', 'set_menu_layout_style')); ?>" data-params="<?php echo esc_attr(OsUtilHelper::build_os_params([], 'set_menu_layout_style')); ?>" class="side-menu-fold-trigger menu-toggler"><i class="latepoint-icon latepoint-icon-menu"></i></a>
<a href="#" title="<?php esc_attr_e('Menu', 'latepoint'); ?>" class="latepoint-mobile-top-menu-trigger">
@@ -24,7 +24,7 @@
echo '<li class="menu-spacer"></li>';
}
continue;
- }
+ }
$sub_menu_html = '';
$is_active = OsRouterHelper::link_has_route($route_name, $menu_item['link']);
@@ -77,4 +77,4 @@
<span><?php esc_html_e('back to WordPress', 'latepoint'); ?></span>
</a>
<?php } ?>
-</div>
No newline at end of file
+</div>
--- a/latepoint/lib/views/services/_form.php
+++ b/latepoint/lib/views/services/_form.php
@@ -313,10 +313,10 @@
if ( OsRolesHelper::can_user( 'service__delete' ) || OsRolesHelper::can_user( 'service__edit' ) ) {
$extra_actions_html.= '<div class="os-trigger-dots"><div class="os-trigger-dots-context">';
if ( OsRolesHelper::can_user( 'service__delete' ) ) {
- $extra_actions_html .= '<div class="os-context-item os-danger" data-os-prompt="' . esc_attr__( 'Are you sure you want to remove this service? It will remove all appointments associated with it. You can also change status to disabled if you want to temprorary disable it instead.', 'latepoint' ) . '"
- data-os-redirect-to="' . esc_url( OsRouterHelper::build_link( OsRouterHelper::build_route_name( 'services', 'index' ) ) ) . '"
- data-os-params="' . esc_attr( OsUtilHelper::build_os_params( [ 'id' => $service->id ], 'destroy_service_' . $service->id ) ) . '"
- data-os-success-action="redirect"
+ $extra_actions_html .= '<div class="os-context-item os-danger os-delete-confirm" data-os-confirm-title="' . esc_attr__( 'Delete Service', 'latepoint' ) . '" data-os-prompt="' . esc_attr__( 'Are you sure you want to remove this service? It will remove all appointments associated with it. You can also change status to disabled if you want to temprorary disable it instead.', 'latepoint' ) . '"
+ data-os-redirect-to="' . esc_url( OsRouterHelper::build_link( OsRouterHelper::build_route_name( 'services', 'index' ) ) ) . '"
+ data-os-params="' . esc_attr( OsUtilHelper::build_os_params( [ 'id' => $service->id ], 'destroy_service_' . $service->id ) ) . '"
+ data-os-success-action="redirect"
data-os-action="' . esc_attr( OsRouterHelper::build_route_name( 'services', 'destroy' ) ) . '"><i class="latepoint-icon latepoint-icon-trash-2"></i><span>' . esc_html__( 'Delete', 'latepoint' ) . '</span></div>';
}
if ( OsRolesHelper::can_user( 'service__edit' ) ) {
--- a/latepoint/lib/views/shared/pro_feature.php
+++ b/latepoint/lib/views/shared/pro_feature.php
@@ -10,4 +10,4 @@
</div>
<a href="<?php echo esc_url(LATEPOINT_UPGRADE_URL); ?>" class="latepoint-pro-link"><?php esc_html_e('Upgrade to paid version', 'latepoint'); ?></a>
<a href="#" class="latepoint-pro-link-subtle" <?php echo OsSettingsHelper::get_link_attributes_for_premium_features(); ?>><?php esc_html_e('Show All premium features', 'latepoint'); ?></a>
-</div>
No newline at end of file
+</div>
--- a/latepoint/lib/views/steps/_booking__services.php
+++ b/latepoint/lib/views/steps/_booking__services.php
@@ -32,11 +32,13 @@
'preselected_category' => $preselected_category,
'preselected_duration' => $preselected_duration,
'preselected_total_attendees' => $preselected_total_attendees,
+ 'bundles' => $bundles,
+ 'service_display_mode' => $restrictions['service_display_mode'] ?? 'all',
] );
}else{
echo '<div class="os-item-categories-holder os-item-categories-main-parent os-animated-parent">';
OsBookingHelper::generate_services_list($services);
- OsBookingHelper::generate_bundles_folder();
+ OsBookingHelper::generate_bundles_folder($bundles, $restrictions);
echo '</div>';
}