Programmatically make a recurring payment

Soumis par victor.bourgade le

Sometimes, you will need to create a recurring payment with Drupal commerce recurring module without using all the usual workflow, like putting a product in your cart, entering the payment method and processing the order.

What you'll want is to trigger this action programmatically, on an ajax call for example. Well, it took me quite a while to figure it out, so here is the detailed explanation of the process how to achieve that.

The thing is commerce_recurring automatically creates subscriptions for products that hold a product variation that allows recurring payment. 

Assuming you are using the "product_variation" subscription type and that you have already created a product that provides a membership subscription holding a recurring product variation:

Product with a recurring product variation

Now let's create an ajax callback in a service that would automatically fetch the product_variation (knowing its ID, here let's say it's 1) and creates the recurring subscription.

/**
 * {@inheritdoc}
 */
function createRecurringPayment($product_variation_id = 1) {
  // Retreive the default store.
  $store = \Drupal::entityTypeManager()->getStorage('commerce_store')->loadDefault();
  // Retreive the product variation which has been configured to be recurring.
  $product_variation = ProductVariation::load($product_variation_id);
  // We then need to programmatically create an order with the product variation that is recurring.
  $current_user = \Drupal\user\Entity\User::load(\Drupal::currentUser()->id());
  // The user is logged in.
  if (!$current_user) {
    return new JsonResponse('You are not logged in');
  }
  // Retreive the first payment method configured by the user.
  $payment_methods = \Drupal::entityTypeManager()->getStorage('commerce_payment_method')->loadByProperties([
    'uid' => $current_user->id(),
  ]);
  if ($payment_method = reset($payment_methods)) {
    $order_item = OrderItem::create([
      'type' => 'default',
      'uid' => $current_user->id(),
      'purchased_entity' => $product_variation->id(),
      'quantity' => 1,
      'unit_price' => $product_variation->getPrice(),
    ]);
    $order_item->save();
    // Add the product variation to a new order.
    $order = Order::create([
      'type' => 'default',
      'uid' => $current_user->id(),
      'order_items' => [$order_item],
      'store_id' => $store->id(),
      'state' => 'draft',
      'payment_method' => $payment_method->id(),
      'email' => $current_user->getEmail(),
    ]);
    $order->save();

    // By setting the state to 'place', commerce_recurring will automatically trigger the first payment and create the subscription for us.
    $order->getState()->applyTransitionById('place');
    $order->save();
    // You might want to return the payment here in case the payment wasn't successful. Pending happens when you have configured your payment gateway to retry after unsuccessful payment and failed, when it has actually failed.
    if ($order->getState() == 'pending' || $order->getState() == 'failed') {
      return new JsonResponse('Failed');
    }
    
  }

  return new JsonResponse('No payment method configured');
}

At this point, we have a fresh "default" order (and not a recurring one) that has been paid and which has triggered the creation of a subscription from commerce_recurring. The important thing to know here is that commerce_recurring does not create "recurring" order before it has actually started to iterate. The first order is always a non-recurring one.

Ok, so now we need to actually start recurring the membership payment and provide additional data to the subscription:

// Just after creating the order, we can retrieve the inactive subscription by fetching it with the initial order id (the one we just created).
$subscriptions = \Drupal::entityTypeManager()->getStorage('commerce_subscription')->loadByProperties([
  'initial_order' => $order->id(),
]);
/** @var \Drupal\commerce_recurring\Entity\Subscription $subscription */
$subscription = reset($subscriptions);
// Automatically activate subscription and reuse the payment method used when paying the first default order.
// This will create a new order of type "recurring" which will be triggered via cron on the due date.
$subscription->setPaymentMethod($payment_method);
$subscription->getState()->applyTransitionById('activate');
$subscription->save();

And that should be all. You should see the first transaction in your payment gateway dashboard (from the default order). For the next dates, the subscription which is now active and has all the information needed to start recurring will trigger a payment every time it was scheduled for.

Hope it can save some of you some time.

Here is the entire function (that you might need to adapt to your need, indeed):

/**
 * {@inheritdoc}
 */
function createRecurringPayment($product_variation_id = 1) {
  // Retreive the default store.
  $store = \Drupal::entityTypeManager()->getStorage('commerce_store')->loadDefault();
  // Retreive the product variation which has been configured to be recurring.
  $product_variation = ProductVariation::load($product_variation_id);
  // We then need to programmatically create an order with the product variation that is recurring.
  $current_user = \Drupal\user\Entity\User::load(\Drupal::currentUser()->id());
  // The user is logged in.
  if (!$current_user) {
    return new JsonResponse('You are not logged in');
  }
  // Retreive the first payment method configured by the user.
  $payment_methods = \Drupal::entityTypeManager()->getStorage('commerce_payment_method')->loadByProperties([
    'uid' => $current_user->id(),
  ]);
  if ($payment_method = reset($payment_methods)) {
    $order_item = OrderItem::create([
      'type' => 'default',
      'uid' => $current_user->id(),
      'purchased_entity' => $product_variation->id(),
      'quantity' => 1,
      'unit_price' => $product_variation->getPrice(),
    ]);
    $order_item->save();
    // Add the product variation to a new order.
    $order = Order::create([
      'type' => 'default',
      'uid' => $current_user->id(),
      'order_items' => [$order_item],
      'store_id' => $store->id(),
      'state' => 'draft',
      'payment_method' => $payment_method->id(),
      'email' => $current_user->getEmail(),
    ]);
    $order->save();

    // By setting the state to 'place', commerce_recurring will automatically trigger the first payment and create the subscription for us.
    $order->getState()->applyTransitionById('place');
    $order->save();
    // You might want to return the payment here in case the payment wasn't successful. Pending happens when you have configured your payment gateway to retry after unsuccessful payment and failed, when it has actually failed.
    if ($order->getState() == 'pending' || $order->getState() == 'failed') {
      return new JsonResponse('Failed');
    }
    // Just after creating the order, we can retrieve the inactive subscription by fetching it with the initial order id (the one we just created).
    $subscriptions = \Drupal::entityTypeManager()->getStorage('commerce_subscription')->loadByProperties([
      'initial_order' => $order->id(),
    ]);
    /** @var \Drupal\commerce_recurring\Entity\Subscription $subscription */
    $subscription = reset($subscriptions);
    // Automatically activate subscription and reuse the payment method used when paying the first default order.
    // This will create a new order of type "recurring" which will be triggered via cron on the due date.
    $subscription->setPaymentMethod($payment_method);
    $subscription->getState()->applyTransitionById('activate');
    $subscription->save();

    return new JsonResponse('success');
  }

  return new JsonResponse('No payment method configured');
}

 

Étiquettes

Drupal 8 Drupal 9 Commerce Commerce recurring

About the writer

victor.bourgade

Victor is a web developer passionnated in drupal and bootstrap technologies. He likes challenges and beautiful designs.

When not behind his computer you'll find him drinking beers with friends or in the middle of nowhere hiking with his dog.