Capturing booking events to enhance workflow with other modules

Created on 30 August 2023, about 1 year ago
Updated 25 May 2024, 6 months ago

Problem/Motivation

I understand from this issue that direct Commerce integration is not a current priority for this module.

Nonetheless, I think it would be possible to achieve this functionality with less effort via the ECA module (especially when BC uses that module for email notifications anyway) if Bookable calendar provided events to be captured by the ECA module. Currently Devel lists no events provided by this module. In view of this, the following events within Bookable calendar seem to be te most important ones:

1. booking an opening instance (with nr of slots / parties as parameters)
2. deleting a booked instance
3. editing a booked instance

These events can then be passed on to ECA either via an ECA submodule within Bookable calendar, or within a custom module. (Even the sending email notification event could be handled by such a submodule.)

Proposed resolution

Commerce integration via ECA would work like this:
1. An entity reference field is added on the opening instance (?) to the commerce product variation (prefilled on content creation or via VBO).
2. ECA workflows are created which capture the respective events (create, edit, delete) and handle the commerce workflow (placing booked product variations into the basket).
3. Normal commerce workflow follows.

I think this functionality would fill a real gap within a Drupal 9->10 world, as the BAT and BEE modules do not currently work well with the newer versions of Drupal and their development is less dynamic than Bookable calendar's.

Remaining tasks

Creating bookable calendar events in code.

Feature request
Status

Active

Version

2.2

Component

Code

Created by

🇮🇪Ireland marksmith

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @marksmith
  • First commit to issue fork.
  • 🇦🇺Australia interlated

    I have a use case where I need to deduct a Commerce Ticket in order to allow people to make a booking.

    A walk through - https://www.youtube.com/watch?v=37Hr3zaSago

    I inserted a make_booking event. The booking has to fail if there are no tickets.

    > BookingContactEvent::EVENT_MAKE_BOOKING

    The ticket event subscriber is

    <?php
    
    namespace Drupal\esl_ticket\EventSubscriber;
    
    use Drupal\bookable_calendar\Event\BookingContactEvent;
    use Drupal\commerce_ticketing\Entity\CommerceTicket;
    use Drupal\Core\Session\AccountInterface;
    use Drupal\esl_ticket\Controller\TicketController;
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    use Drupal\Core\Entity\EntityTypeManagerInterface;
    use Drupal\Core\StringTranslation\StringTranslationTrait;
    
    /**
     * Generate a Tranche id once a deal is published.
     *
     * Depends on cbi_deal_count to set enable selection of deal reference bonds
     * that match the issuer.
     *
     */
    class EslTicketCalendarSubscriber implements EventSubscriberInterface {
    
      // 'cancel' in the ticket
      // Based on the workflow options. Badly coded in the controller as it will
      // ignore not matching states.
      const TICKET_USED = 'used';
    
      const TICKET_ACTIVE = 'active';
    
      use StringTranslationTrait;
    
      protected EntityTypeManagerInterface $entityTypeManager;
    
      protected AccountInterface $user;
    
      protected $ticketController;
    
      /**
       * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
       * @param \Drupal\Core\Session\AccountInterface $user
       */
      public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountInterface $user) {
        $this->entityTypeManager = $entity_type_manager;
        $this->user = $user;
      }
    
      /*
       * Insert a booking, take a ticket. Which ticket??
       */
      public static function getSubscribedEvents(): array {
        return [
          BookingContactEvent::EVENT_MAKE_BOOKING => [
            'redeemTicket',
            -20,
          ],
        ];
      }
    
      /**
       * Add a new bond. Does not respond to new issuer as unless the issuer is
       * related to a bond it doesn't matter.
       *
       * Entity update and entity insert events are called at the same time.
       *
       * @param \Drupal\bookable_calendar\Event\BookingContactEvent $event
       *
       * @return void
       * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
       * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
       * @throws \Drupal\Core\Entity\EntityStorageException
       */
      public function redeemTicket(BookingContactEvent $event): void {
        // party_size for the number of tickets needed.
        $booking = $event->getBooking();
        $bundles = [
          'booking_contact',
        ];
    
        if (!in_array($booking->bundle(), $bundles)) {
          return;
        }
    
        // The ticket should be of a type as stipulated by the booking.
        // Check to see if a booking_type field is set.
    
        // CommerceTicket
    
        $this->ticketController = new TicketController();
    
        $party_size = $booking->get('party_size')->getValue()[0]['value'];
        $active_tickets = $this->getUsersTickets();
    
        if (empty($active_tickets) || count($active_tickets) < $party_size) {
          throw new \InvalidArgumentException('You need to buy a ticket to make this booking');
        }
    
        $used_tickets = [];
        for ($a_party = 0; $a_party < $party_size; $a_party++) {
          try {
            $used_tickets[] = $this->useTicket(array_pop($active_tickets));
          }
          catch (\InvalidArgumentException $exception) {
            $this->ticketsToActive($used_tickets);
            throw new \InvalidArgumentException($exception->getMessage());
          }
        }
      }
    
      /**
       * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
       * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
       */
      protected function useTicket($ticket): CommerceTicket {
        $valid = $this->ticketController->check(
          $ticket->getPurchasedEntity()->getProduct(),
          $ticket,
          $ticket->getPurchasedEntity());
    
        if (!$valid) {
          throw new \InvalidArgumentException('You need to buy a ticket to make this booking');
        }
    
        // Calls check again. If not valid then
        $setStatusResult = $this->ticketController->setStatus($ticket->getPurchasedEntity()
          ->getProduct(),
          $ticket,
          $ticket->getPurchasedEntity(), self::TICKET_USED);
    
        // StatusResult failed, no booking allowed
        $response = json_decode($setStatusResult->getContent());
        if (property_exists($response, 'state') && $response->state == self::TICKET_USED) {
          return $ticket;
        }
    
        throw new \InvalidArgumentException('You need to buy a ticket to make this booking');
      }
    
      protected function ticketsToActive(array $tickets): void {
        foreach ($tickets as $rollback) {
          $setStatusResult = $this->ticketController->setStatus($rollback->getPurchasedEntity()
            ->getProduct(),
            $rollback,
            $rollback->getPurchasedEntity(), self::TICKET_ACTIVE);
          $response = json_decode($setStatusResult->getContent());
          if (!property_exists($response, 'state') || $response->state != self::TICKET_ACTIVE) {
            \Drupal::messenger()
              ->addError(t('Could not rollback ticket %ticket. Please contact the site administrators.', ['%ticket' => $rollback->id()]));
          }
        }
      }
    
      /**
       *
       * @return \Drupal\commerce_ticketing\Entity\CommerceTicket[]|\Drupal\Core\Entity\EntityBase[]|\Drupal\Core\Entity\EntityInterface[]
       * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
       * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
       */
      protected function getUsersTickets(): array {
        $ids = \Drupal::entityQuery('commerce_ticket')
          ->accessCheck(TRUE)
          ->condition('uid', $this->user->id())
          ->condition('state', self::TICKET_ACTIVE)
          ->execute();
    
        return CommerceTicket::loadMultiple($ids);
      }
    
    }
    
  • 🇬🇧United Kingdom problue solutions Northern Ireland

    I have a question related to this. I will be using Stripe to take a payment when the book button is clicked, the booking will only be created upon sucessful payment. I want to do this is the simpliest and most basic way possible without using Drupal Commerce or the need to create products etc.

    My question is where is the appropriate place to trigger the Stripe checkout session, is it in the save() function of BookingForm.php?

  • 🇬🇧United Kingdom problue solutions Northern Ireland

    So i have this kind of working by overriding the instanceBookLink() method in the Renderer service, we send the user to Stripe to make payment and then return to the bookable_calendar.booking_contact.create route to complete the booking. This works.

    Theres an extra step after making payment though, where the user must review/confirm the booking contact before clicking book again to finalise the booking. Really what needs to happen is how the on-click booking works, where there is no confirmation of the booking contact and the logged in user's details are used.

    If i enable one-click bookings then directing to bookable_calendar.booking_contact.create on successful payment doesnt work anymore. Directing to bookable_calendar.ajax.booking_contact.create doesnt work either due to the Ajax stuff going on that Stripe interupts.

    Any ideas on how to do this using the existing methods of bookable calendar without unnecessarily overiding large parts of the code?

Production build 0.71.5 2024