billwerk_subscriptions-1.x-dev/src/EventSubscriber/BillwerkWebhookEventSubscriber.php

src/EventSubscriber/BillwerkWebhookEventSubscriber.php
<?php

declare(strict_types=1);

namespace Drupal\billwerk_subscriptions\EventSubscriber;

use Drupal\billwerk_subscriptions\BillwerkDataObjectFactory;
use Drupal\billwerk_subscriptions\Event\BillwerkWebhookEvent;
use Drupal\billwerk_subscriptions\Event\SubscriberContractChangedEvent;
use Drupal\billwerk_subscriptions\Event\SubscriberCustomerChangedEvent;
use Drupal\billwerk_subscriptions\Exception\CustomerExternalIdEmptyException;
use Drupal\billwerk_subscriptions\Exception\WebhookException;
use Drupal\billwerk_subscriptions\LogHelper;
use Drupal\billwerk_subscriptions\Subscriber;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Subscriber to incoming Billwerk webhook calls.
 *
 * As the individual logic should not be implemented here in the general
 * module, this is just used to trigger more specific events and handle
 * "Test" webhook calls.
 */
final class BillwerkWebhookEventSubscriber implements EventSubscriberInterface {

  /**
   * The constructor.
   *
   * @param \Drupal\billwerk_subscriptions\BillwerkDataObjectFactory $billwerkDataObjectFactory
   *   The Billwerk Data Object Factory.
   * @param \Drupal\billwerk_subscriptions\LogHelper $logHelper
   *   The Log Helper.
   * @param \Psr\EventDispatcher\EventDispatcherInterface $eventDispatcher
   *   The Event Dispatcher.
   */
  public function __construct(
    protected readonly BillwerkDataObjectFactory $billwerkDataObjectFactory,
    protected readonly LogHelper $logHelper,
    protected readonly EventDispatcherInterface $eventDispatcher,
  ) {
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    // NOTE: We do not list or subscribe to all available webhooks here.
    // Just the ones that are likely to be relevant.
    // Of course all available Billwerk webhooks can be used like this.
    // The webhooks documentation can be found here:
    // @see https://docu.billwerk.plus/api/premium-enterprise/en/webhooks.html
    return [
      BillwerkWebhookEvent::EVENT_NAME_PREFIX . 'ContractChanged' => 'onContractChanged',
      BillwerkWebhookEvent::EVENT_NAME_PREFIX . 'CustomerChanged' => 'onCustomerChanged',
      BillwerkWebhookEvent::EVENT_NAME_PREFIX . 'Test' => 'onTest',
    ];
  }

  /**
   * Customer Changed.
   *
   * Sent whenever the base information of a customer has changed because it was
   * modified via the customer portal.
   *
   * Typical Scenario:
   * Usually, you want to listen to this hook to keep your customer data in sync
   * between your system and billwerk. To do so, fetch the customer by id and
   * apply all necessary changes on your side.
   *
   * Example:
   * ```
   * {
   *     "CustomerId": "62a86d3a826fa0a7d0d07032",
   *     "Event": "CustomerChanged",
   *     "EntityId": "62148e3c0c14e1609e9ca5c1"
   * }
   * ```
   *
   * @param \Drupal\billwerk_subscriptions\Event\BillwerkWebhookEvent $event
   *   The BillwerkWebhook event.
   *
   * @see https://docu.billwerk.plus/api/premium-enterprise/en/webhooks/customer-and-contract/customer-changed.html
   */
  public function onCustomerChanged(BillwerkWebhookEvent $event): void {
    // We don't implement specific logic here, instead we let submodules do that
    // by the specific event:
    $customerId = $event->extractDataValue('CustomerId');
    if (empty($customerId)) {
      // This webhook should always provide a ContractId:
      throw new WebhookException("Expected CustomerId to be given, but CustomerId was empty: \"{$customerId}\"");
    }
    try {
      $billwerkCustomer = $this->billwerkDataObjectFactory->billwerkLoadBillwerkCustomer($customerId);
    }
    catch (CustomerExternalIdEmptyException $e) {
      // This customer has no ExternalId, which means has no Drupal account.
      // Such accounts are not treated as Drupal-relevant customers,
      // so we return here.
      return;
    }
    // We only handle customers with an ExternalCustomerId (= Drupal UID) set.
    $externalCustomerId = $billwerkCustomer->getExternalCustomerId();
    // We need to cast the $externalCustomerId to int here as Drupal UID.
    // But we ensure it's not UID=0 afterwards.
    if (!empty($externalCustomerId) && !empty((int) $externalCustomerId)) {
      $subscriber = Subscriber::loadByDrupalUid((int) $externalCustomerId);
      if (!empty($subscriber)) {
        $subscriberCustomerChangedEvent = new SubscriberCustomerChangedEvent($subscriber);
        $this->eventDispatcher->dispatch($subscriberCustomerChangedEvent);
      }
      else {
        // If an external customer id is set, we expect a Drupal user to exist
        // for it, so throw an Exception, if none could be loaded.
        throw new WebhookException("Subscriber user account for Billwerk Customer ID #{$customerId} with ExternalCustomerId #{$externalCustomerId} could not be found. No such user exists. Could not update the user subscription.");
      }
    }
  }

  /**
   * Contract Changed.
   *
   * Sent whenever the state of a contract has changed. There are various
   * reasons why the contract could have changed, e.g.:
   * - The trial has ended
   * - The contract has ended
   * - The contract was up- or downgraded
   * - A component subscription was added or changed.
   *
   * Typical Scenario:
   * You want to listen to this hook to make sure your customers get what they
   * ordered (and only what they paid for). Together with Contract Created, this
   * is the most important hook. To find out what the current state of the
   * contract is, simply fetch the referenced contract by id and make sure to
   * configure your service to deliver what is configured in the contract.
   *
   * Contract Change types
   * - Signup
   * - Upgrade
   * - TrialEndChange
   * - ComponentSubscriptionChange
   * - DiscountSubscriptionChange
   * - Timebased
   * - EndContract, Annulation
   * - Pause, Resume
   * - ExternalSubscriptionSync
   * As multiple changes can be performed via an order and an order will lead to
   * an Upgrade contract change - it is not enough to rely on a single type like
   * e.g. ComponentSubscriptionChange. The type is intended to be used to
   * opt-out early scenarios.
   *
   * All contract change webhooks contain the fields ContractId,
   * ContractChangedType and ContractChangeId. To access more details about what
   * has happened in the contract, call the REST API to get all the details of
   * the contract change.
   *
   * Example values:
   * ```
   * {
   *     "ContractId": "645b429bb73d36f442b223df",
   *     "CustomerId": "645b429bb73d36f442b223db",
   *     "ExternalCustomerId": "249969",
   *     "ContractChangeId": "645b42d7f85fee194bce8990",
   *     "ContractChangeType": "Upgrade",
   *     "Event": "ContractChanged",
   *     "EntityId": "63b2d4405b49105c19fa7714"
   * }
   * ```
   *
   * @param \Drupal\billwerk_subscriptions\Event\BillwerkWebhookEvent $event
   *   The BillwerkWebhook event.
   *
   * @see https://docu.billwerk.plus/api/premium-enterprise/en/webhooks/customer-and-contract/contract-changed.html
   */
  public function onContractChanged(BillwerkWebhookEvent $event): void {
    // We don't implement specific logic here, instead we let submodules do that
    // by the specific event:
    $contractId = $event->extractDataValue("ContractId");
    if (empty($contractId)) {
      // This webhook should always provide a ContractId:
      throw new WebhookException("Expected ContractId to be given, but ContractId was empty: \"{$contractId}\"");
    }
    // !IMPORTANT! Be careful with $contractChangeType and read the
    // method comment and the Billwerk Documentation, as it doesn't works
    // as one would expect.
    $contractChangeType = $event->extractDataValue("ContractChangeType");

    if (in_array($contractChangeType, [SubscriberContractChangedEvent::CONTRACT_CHANGE_TYPE_SIGNUP])) {
      // Skip for the listed change types as we don't want to trigger the event
      // in these cases.
      // Skipping is in the way the documentation allows us to use the
      // $contractChangeType!
      $this->logHelper->debug("Skipping for explicitly unhandled ContractChangeType: \"{$contractChangeType}\".", [], $event->getData(), __CLASS__);
      return;
    }

    // Dispatch the specific event:
    $subscriber = Subscriber::loadByContractId($contractId);
    if (empty($subscriber)) {
      // We should not expect a contract to be present in Drupal for each
      // Billwerk contract. So we shouldn't throw an exception here but just
      // return if not found.
      // So we just log this for debugging:
      $this->logHelper->debug("No contract with ID: \"{$contractId}\" could be found.", [], $event->getData(), __CLASS__);
      return;
    }
    $subscriberContractChangedEvent = new SubscriberContractChangedEvent($subscriber, $contractChangeType);
    $this->eventDispatcher->dispatch($subscriberContractChangedEvent);
  }

  /**
   * Handles a test event.
   */
  public function onTest(BillwerkWebhookEvent $event): void {
    // Just write a log entry to show it works as expected.
    $this->logHelper->debug('Billwerk Test Webhook received!', [], $event->getData(), __CLASS__);
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc