Another far-future task. In the CLB lifecycle there have been many people who wanted a "manual" collection method/charging strategy/whatever. Automatic means we always auto-charge using the payment method associated with a subscription, manual means that the customer is invoiced for the just-closed recurring order, pays in a manual way (such as bank transfer), after which the administrator closes the recurring order.

Once Commerce gets entity_print integration, it won't be hard to pair that with Manual payment gateways, and complete the flow.

Comments

bojanz created an issue. See original summary.

Drupa1ish’s picture

1)

Once Commerce gets entity_print integration

Do you mean #2831952-41: Create an entity_print renderer for orders (to allow order PDF output). Because it seems to be almost ready.

2) This will cover also off-site payments?

3) Another approach would be to pass every kind of payment ( onsite, off-site, manual) via commerce funds/account balance #2885591: add commerce funds feature.
This is how Blesta ( https://www.blesta.com/ billing software for hosting providers) works.
Even onsite payments deposit money in commerce balance, and the orders are fulfilled on cron.

Using this unified solution, the scenario will be:
a) I receive renewal notifications #2925892: Renewal Notification
b) If I have manual/off-site payment, I load my balance account, after notification received. With onsite, nothing to do.
c) When the scheduled billing is dued, the recurring order is fulfilled from balance. If not enough funds, the dunning will always try from balance.

jonathanshaw’s picture

Title: Allow manual renewal of subscriptions » Work with manual or offsite payments

The current implementation only supports onsite payments, where payment is automatically and instantly collected. It would be good to expand to work with offsite and manual payment methods where the payment is either not automatic or not instant. This could include payment by bank transfer, payment from a site fund, or payment via Stripe or Paypal Subscriptions API.

Currently run() in Cron.php sets up for each order a call to RecurringOrderManager->closeOrder()

  public function closeOrder(OrderInterface $order) {
    if ($order->getState()->value == 'draft') {
      $transition = $order->getState()->getWorkflow()->getTransition('place');
      $order->getState()->applyTransition($transition);
      $order->save();
    }

    $payment_method = $this->selectPaymentMethod($order);
    if (!$payment_method) {
      throw new HardDeclineException('Payment method not found.');
    }
    $payment_gateway = $payment_method->getPaymentGateway();
    /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OnsitePaymentGatewayInterface $payment_gateway_plugin */
    $payment_gateway_plugin = $payment_gateway->getPlugin();
    $payment_storage = $this->entityTypeManager->getStorage('commerce_payment');
    /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
    $payment = $payment_storage->create([
      'payment_gateway' => $payment_gateway->id(),
      'payment_method' => $payment_method->id(),
      'order_id' => $order->id(),
      'amount' => $order->getTotalPrice(),
      'state' => 'new',
    ]);
    // The createPayment() call might throw a decline exception, which is
    // supposed to be handled by the caller, to allow for dunning.
    $payment_gateway_plugin->createPayment($payment);

    $transition = $order->getState()->getWorkflow()->getTransition('mark_paid');
    $order->getState()->applyTransition($transition);
    $order->save();
  }

So the closeOrder function is doing quite a few things, including collecting the money via an assumed onsite payment.

I suggest ...

1) run() should be able to call some method on the recurringOrderManager - even if the payment gateway is manual - to communicate the fact that the order is due for payment. Therefore I suggest adding a placeOrder() method on the recurringOrderManager. The default implementation can set the workflow state to "place".

2) collecting payment should probably happen on placeOrder() not closeOrder() (and only if the gateway implements SupportsStoredPaymentMethodsInterface);

3) closeOrder() called by cron shouldn't be marking orders as paid if we don't have the ability to verify at the time of processing that the payment has been completed, i.e. for manual and offsite orders. Therefore run() should call placeOrder() not closeOrder(). placeOrder() should call closeOrder() after a successful payment if using SupportsStoredPaymentMethodsInterface.

4) If it's important to maintain BC for beta adopters who are extending RecurringOrderManager or invoking closeOrder() independently, then maybe we can hack some BC support into closeOrder().

So run() calls placeOrder(), which marks the order state as 'place', collects payment if appropriate, then calls closeOrder() which marks the order as paid. The point of splitting the methods is to allow for gateways (like manual) that separate initiating a payment from knowing it is received; they may need to respond to an order being due for payment (state='place'), and then later tell recurring that the payment is received and the order can be closed (state='mark_paid').


How does this sound? Does this seem like the right approach?

One additional thing to consider: sometimes customers pay in weird exceptional ways. For example, if their credit card is failing one month they might send a cheque. What's the right way to handle payments using a different payment method to that which is normal for the recurring?

bojanz’s picture

Title: Work with manual or offsite payments » Allow manual renewal of subscriptions

Please don't expand the scope of existing feature requests. You're free to open a new issue for additional requirements.
I'm saying this because turning a 1-day issue into a 2-day issue can sometimes delay it by months, since it's now less practical for contributors to handle.

In general, supporting offsite payment gateways will be possible once Commerce expands the offsite API to allow payment method creation: #2838380: [META] Allow offsite payment gateways to create and use payment methods. Once that issue lands, a Recurring issue can expand the existing OnsitePaymentGatewayInterface check to be a SupportsPaymentMethodsInterface check.

payment via Stripe or Paypal Subscriptions API.

Supporting external Subscription APIs is a very different task from supporting manual and offsite gateways. It would require an order of magnitude more work.

jonathanshaw’s picture

Sorry Bojanz, I'm not very familiar with all the subtleties here. I retitled because I thought this issue unblocked other things in this area.

There is no SupportsPaymentMethodsInterface, I presume you mean SupportsStoredPaymentMethodsInterface; I've edited my #3 proposal to use that interface.

Does #3 seem like a reasonable direction to make the recurring API serve the use case of manual payments?

It seems like you mention #2838380: [META] Allow offsite payment gateways to create and use payment methods only because I mentioned offsite, it's not relevant to manual gateways and this issue.

Supporting external Subscription APIs is a very different task from supporting manual and offsite gateways. It would require an order of magnitude more work.

I mention it because as far as I can think most of that work straightforwardly belongs in a 3rd party module, but this is the place I've thought of so far where recurring may make assumptions that conflict with this use case.

recidive’s picture

Issue tags: +Release blocker

Let's make sure we add this for 1.0. Patches welcome.

jonathanshaw’s picture

jeff veit’s picture

See https://www.drupal.org/project/commerce_recurring/issues/3203545 . This goes some way towards allowing manual renewal.

recidive’s picture

Assigned: Unassigned » recidive
Status: Active » Needs review
StatusFileSize
new3.64 KB

Attached is a first shot at this. This works for the use case of the project we are working on.

The flow is creating the initial order, paying the order with a manual payment and placing it. The subscription is created without a payment method, but admins can pay the recurring orders before the end of the billing cycle so the subscription renews otherwise it gets to the dunning cycle.

Status: Needs review » Needs work

The last submitted patch, 10: 2919602-10-allow-non-reusable-payments.patch, failed testing. View results

jonathanshaw’s picture

It's remarkable how elegant this is.

My main concern is that it opens the door for quite a wide range of use cases other than automatically charging a stored commerce_payment payment method, BUT the terminology used of 'allow non-reusable payments' is unclear and maybe too closely coupled with the use case described.

How about 'requires reusable payment method' and requiresReusablePaymentMethod()

+++ b/src/EventSubscriber/OrderSubscriber.php
@@ -87,7 +87,7 @@ class OrderSubscriber implements EventSubscriberInterface {
+      if (!$billing_schedule->getPlugin()->allowTrials() && !$billing_schedule->getPlugin()->allowNonReusablePayments() && empty($payment_method)) {

I wonder if BillingScheduleInterface should also have a requiresReusablePaymentMethodInitially() method so this logic can be contained on the BillingSchedule.

jsacksick’s picture

requiresReusablePaymentMethodInitially is a bit wordy...

What about shortening this to just requiresPaymentMethod()? Also, I'm not sure the billing schedule makes the most sense for this setting, however, I don't think we can put this somewhere else.

Regarding the condition it'd probably read better like the following:

if (empty($payment_method) && (!$billing_schedule->getPlugin()->allowTrials() || $billing_schedule->getPlugin()->requiresPaymentMethod())

By the way we could probably have a $billing_schedule_plugin variable too :p.

The problem I have with this setting is that it's just checked once, in the placed subscriber.

The setting basically means: "Proceed to the subscription creation if no payment method was collected in checkout". (I know this setting is not just to address checkout but also orders created by CSR).

jsacksick’s picture

Just read the initial issue description and I'm not sure the patch makes sense for that particular issue.

The title of the issue says "Allow manual renewal of subscriptions" while this patch addresses only the initial creation of the subscription.

司南’s picture

This also block the charge by using non-payment-method-supported payment gateways,
for example, WeChat native-pay/jspai-pay/h5-pay.
Almost all people use WeChat payment in China, it's important to support this feature.

See also Issues#3052677: Add off-site payment support

司南’s picture

jaime@gingerrobot.com’s picture

I need a solution like this to manually add a subscription to users for free. They will not have a payment method and we'll possibly give the next year free too, or perhaps they will need to sign up with a credit card. Still to be decided.

For now I changed:

  • The comment above the section that checks if the non_reusable payments is allowed.
  • I check if there is even a config setting for non_reusable payments, because in my cause I already have many subscriptions set up.

Note: To use this change I need to create a brand new subscription.

jaime@gingerrobot.com’s picture

StatusFileSize
new5.13 KB

I had a go at updating the tests.

jaime@gingerrobot.com’s picture

StatusFileSize
new4.93 KB
jaime@gingerrobot.com’s picture

Status: Needs work » Needs review