Implementation guide

Last updated on
13 October 2019

Commerce Recurring Metered Billing is an API, and as such does not provide any new user interface elements. Instead, it provides storage, interfaces, base classes, and plugins to implement metered billing much more quickly than from scratch.

In broad terms, it:

  • Provides a database table and storage class for usage tracking
  • Provides plugins (usage types) for Counter and Gauge-type usage
  • Provides a base class that includes tracked usage in the charges reported to Commerce Recurring
  • Provides a helper service to make recording and checking usage more straightforward

Standard implementation

Note: References to "mymodule" in this subsection refer to your custom module that you're using to implement usage tracking.

Step 1

First, figure out which subscription type you are using. You can find this by editing the product variation containing the subscription that users purchase. You may have to search the codebase to figure out which class provides it. It will be something implementing \Drupal\commerce_recurring\Plugin\Commerce\SubscriptionType\SubscriptionTypeInterface.

Most people, though, will likely be using \Drupal\commerce_recurring\Plugin\Commerce\SubscriptionType\ProductVariation or \Drupal\commerce_license\Plugin\Commerce\SubscriptionType\LicenseSubscription.

Step 2

If you're using ProductVariation:

In your custom module, create a new class in the src/Plugin/Commerce/SubscriptionType subdirectory of your module with the namespace Drupal\mymodule\Plugin\Commerce\SubscriptionType. You should choose the name for the class, but if you are unsure, call it ProductVariationWithUsage. In it, extend \Drupal\commerce_recurring\Plugin\Commerce\SubscriptionType\MeteredBillingBase.

Note how we did not extend ProductVariation itself. That is because it does not actually contain any unique code, and this allows you to take advantage of MeteredBillingBase::collectCharges(). You'd have to implement it yourself otherwise.

Be sure to copy the plugin annotation from ProductVariation and change the ID to something else so that Drupal's plugin discovery system finds your subscription type.

If you're using LicenseSubscription (or another subscription type that actually has its own behaviors):

In your custom module, create a new class in the src/Plugin/Commerce/SubscriptionType subdirectory of your module with the namespace Drupal\mymodule\Plugin\Commerce\SubscriptionType. You should choose the name for the class, but if you are unsure, call it ProductVariationWithUsage. In it, extend \Drupal\commerce_license\Plugin\Commerce\SubscriptionType\LicenseSubscription (or your actual subscription type class).

Make sure this code is in your subclass:

/**
 * The usage proxy.
 *
 * @var \Drupal\commerce_recurring_metered\UsageProxyInterface
 */
protected $usageProxy;

/**
 * {@inheritdoc}
 */
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, UsageProxyInterface $usage_proxy) {
  parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
  $this->usageProxy = $usage_proxy;
}

/**
 * {@inheritdoc}
 */
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  return new static(
    $configuration,
    $plugin_id,
    $plugin_definition,
    $container->get('entity_type.manager'),
    $container->get('commerce_recurring_metered.usage_proxy'));
}

/**
 * {@inheritdoc}
 */
public function collectCharges(SubscriptionInterface $subscription, BillingPeriod $billing_period) {
    $charges = parent::collectCharges($subscription, $billing_period);
    $usage_charges = $this->usageProxy->collectCharges($subscription, $billing_period);

    return array_merge($charges, $usage_charges);
  }

This assumes a standard implementation. It retrieves the UsageProxy helper class from the service container and makes it accessible, and then uses it in ::collectCharges() to ensure Commerce Recurring is informed about any tracked usage (it generates charges for it).

Be sure to copy the plugin annotation from the parent class and change the ID to something else so that Drupal's plugin discovery system finds your new subscription type. See the next step for an example of the annotation.

Step 3

Note: This and following steps assume that you extended ProductVariation, but the steps apply to both parent classes.

Depending on the usage type (see the built-in usage types), you may have to implement additional interfaces. Currently, only the counter usage type requires your subscription type to implement \Drupal\commerce_recurring_metered\SubscriptionFreeUsageInterface. You have to return the amount of free usage allowed for the given product variation (subscription product). The number can be zero, but it's usually not for prepaid subscriptions, as users typically pay for a certain upfront allowance before being charged for overages.

Typically, you'll add this as a field to the product variation type and set it when creating the variation. You can then get the value in code and return it as an integer. However, your setup may vary, so Commerce Recurring Metered Billing does not make any assumptions.

At this point, you should have a class that looks something like this:

namespace Drupal\commerce_recurring_metered_test\Plugin\Commerce\SubscriptionType;

use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\commerce_recurring\Entity\SubscriptionInterface;
use Drupal\commerce_recurring_metered\Plugin\Commerce\SubscriptionType\MeteredBillingBase;
use Drupal\commerce_recurring_metered\SubscriptionFreeUsageInterface;

/**
 * Test class for usage tracking against a product variation.
 *
 * @CommerceSubscriptionType(
 *  id = "usage_test_product_variation",
 *  label = @Translation("Usage test product variation."),
 *  purchasable_entity_type = "commerce_product_variation",
 * )
 */
class UsageTestProductVariation extends MeteredBillingBase implements SubscriptionFreeUsageInterface {

  /**
   * {@inheritdoc}
   */
  public function getFreeQuantity(ProductVariationInterface $variation, SubscriptionInterface $subscription) {
    if ($variation->getSku() === 'variation_300_free') {
      return 300;
    }

    if ($variation->getSku() === 'variation_5_free') {
      return 5;
    }

    return 0;
  }

}

Step 4

Rebuild the cache (Config > Development > Performance or with Drush/Drupal Console). Edit the product variation containing your subscription and ensure the subscription type is set to your new one. Save.

Step 5

You're now ready to track usage. This is the part that is most custom and depends heavily on your implementation, so it's not possible to provide code that you can copy and paste. However, you will generally:

  1. Add an event subscriber to the module where you implemented the new subscription type. In this event subscriber, listen for the event emitted by your code after it has successfully done something that affects usage. For example, (counter) you have just fulfilled an API request or (gauge) the user has just saved changes that affect their hosting plan. Note: You might have to add code to dispatch this event. You might also not use events at all and just add the usage directly in the processing code. It depends on whether you separate the Commerce-related functions from your API itself.
  2. (Recommended) Ensure your code has access to \Drupal\commerce_recurring_metered\UsageProxy (for example, inject it into your class or add it as an argument to your service).
  3. (with UsageProxy) Call $this->usageProxy->addUsage($usage, DrupalDateTime $start, DrupalDateTime $end) to add your usage. The usage proxy makes it fairly simple. Here is an example for Counter usage:
     
        $subscription_storage = $this->entityTypeManager->getStorage('commerce_subscription');
        $subscription_query = $subscription_storage->getQuery();
        $user_subscriptions = $subscription_query
          ->condition('uid', $this->currentUser->id())
          ->condition('state', 'active')
          ->sort('subscription_id', 'DESC')
          ->range(0, 1)
          ->execute();
    
        /** @var \Drupal\commerce_recurring\Entity\SubscriptionInterface $user_subscription */
        $user_subscription = $subscription_storage->load(reset($user_subscriptions));
        /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $usage_variation */
        $subscription_product = $this->entityTypeManager->getStorage('commerce_product_variation')->load($user_subscription->getPurchasedEntityId());
        $usage_variation = $subscription_product->get('field_metered_usage_product')
          ->first()
          ->get('entity')
          ->getTarget()
          ->getValue();
    
        $this->usageProxy->addUsage($user_subscription, $usage_variation);

    (Counter usage is the default for UsageProxy::addUsage(). $this->entityTypeManager and $this->currentUser are injected from the service container outside of the code example. Browse the code to see all the options.)

    If you are not using the usage proxy, the simplest approach is to look at what it does and do that directly in your code. It instantiates the relevant usage type plugin and then calls ::addUsage() with the argument format the built-in plugin types expect.

Once you have added usage (over and above any free allowances), you should be able to see it on the draft order for the subscriptions on the Store > Orders page. You're done!

Considerations for advanced implementations

Advanced implementations are for situations where the defaults don't do enough for what you need. The most common advanced implementation would involve creating a custom usage type. For example, in a per-hour billing hosting scenario, you might want the Gauge usage type itself to automatically "block" usage into hours.

Usage types are just implementations of \Drupal\commerce_recurring_metered\UsageTypeInterface. You can implement your own from scratch or extend one of the existing plugins. Be sure to specify the plugin ID when calling ::addUsage() and other methods that need it. It might also be helpful to read the tests in  tests/src and to read the \Drupal\commerce_recurring_metered\UsageProxy code in general, particularly if your ::addUsage() method needs additional arguments or is otherwise incompatible with the usage proxy's expectations. You can, of course, always subclass or copy the usage proxy into your own helper service implementing \Drupal\commerce_recurring_metered\UsageProxyInterface. You are the best to decide if that makes sense for your implementation.

Help improve this page

Page status: No known problems

You can: