Require creating payment method on free orders

Created on 16 August 2024, about 1 month ago
Updated 11 September 2024, 5 days ago

Describe your bug or feature request.

This issue originates in 📌 Add checkout settings for payment method behavior Needs review but for the sake of limiting the scope and moving it ahead, I am creating a separate feature request.

The idea is that in some scenarios, for example for recurring subscriptions with a free trial, the first billing period is free but all the following billing periods require payment. Unfortunately, we are not able to collect reusable payment instrument (aka payment method) for subsequent charges if there's no initial payment - the payment information pane doesn't generate a payment form if the order total amount is zero.

For backward compatibility reasons, this should be an opt-in option so that current Drupal Commerce instances are not affected.

📌 Add checkout settings for payment method behavior Needs review already has a working patch that allows for collecting payment methods on free orders which will be used as a base here. That being said, not all payment methods will allow creating payment methods based on free orders (total 0,-) therefore each compatible payment gateway should be annotated (most likely using a common interface).

Feature request
Status

Fixed

Version

3.0

Component

Payment

Created by

🇳🇴Norway zaporylie

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

Merge Requests

Comments & Activities

  • Issue created by @zaporylie
  • 🇮🇱Israel jsacksick

    Just wanted to provide some input here. Some payment gateways like Braintree forbid "Negative option billing".
    See https://developer.paypal.com/braintree/articles/guides/recurring-billing... for more information.

    It's generally not recommended to start charging a customer after a free trial, especially if we're failing to notify prior to the charge, or if we don't offer a way to opt out...
    Not really sure that affects us, as this is probably on the merchant shoulder to solve, but figured I'd share this anyway.

  • 🇳🇴Norway zaporylie

    Created MR with a functional draft.

  • Pipeline finished with Success
    about 1 month ago
    Total: 459s
    #256032
  • Pipeline finished with Success
    29 days ago
    Total: 1296s
    #257837
  • 🇳🇴Norway zaporylie

    I improved the interface comment for the SupportsFreeOrdersInterface.

    Let me elaborate on the motivations here. (1) Both Onsite and Offsite payment gateways should be allowed to proceed through checkout and collect payment methods. This is why SupportsFreeOrdersInterface inherits from SupportsStoredPaymentMethodsInterface. (2) The SupportsFreeOrdersInterface interface doesn't define any methods at the moment as none is strictly necessary to proceed with free order. However, each payment gateway that supports free orders will likely have to adapt its code to explicitly support free orders. (a) ::createPaymentMethod() the API call for creating Payment Method may need modification depending if the total amount is a positive number or zero (b) ::createPayment() may need to omit to create payment if the amount is zero, (c) ::onReturn() and ::onNotify() as well as PaymentOffsiteForm::buildConfigurationForm may need certain adjustments to support free orders. (3) We may want to use SupportsFreeOrdersInterface for defining a method that would show helpful text in the checkout. Ie. "Your credit card will be authorized during the checkout process and stored for future payment" or "You may notice 1EUR authorization on your credit card that will be reverted immediately. This is required to validate your payment details.". I don't think it's strictly necessary and can be easily done by the merchant by customizing their checkout to include such information. (4) FreeOrderPaymentOptionsSubscriber is taking care of filtering incompatible payment options from the payment options list available to customers. The filtering is happening only if PaymentInformation checkout pane was configured to collect payments on free orders.

  • 🇮🇱Israel jsacksick

    We need tests coverage, and we should probably have either the Onsite payment gateway and/or the example_stored_offsite_redirect implement this interface so we can test that the changes are working as expected.

    We need to test that the payment information pane is visible, that a payment method is collected etc... I think PaymentCheckoutTest would be the right fit for that.

    And we also need a change record, to inform gateway developers of the change.

    Let's rename the subscriber service to commerce_payment.free_order_payments_options_subscriber

  • 🇳🇴Norway zaporylie

    Renamed the service and added support to example_stored_offsite_redirect and example_onsite. Given different gateways may need to approach implementation differently example_onsite does create payment even if the payment amount is 0,- and example_stored_offsite_redirect creates a payment method but never creates a payment if the payment amount is 0,-.

    I will add automated tests later today and work on the CR

  • Pipeline finished with Success
    28 days ago
    Total: 425s
    #258007
  • Status changed to Needs work 28 days ago
  • 🇮🇱Israel jsacksick

    I just realized that the subscriber doesn't even check the order total.

  • 🇮🇱Israel jsacksick

    I'm now shared and wondering whether we shouldn't adopt an approach similar to the "requires_billing_information" boolean.
    Whenever a payment gateway explicitly say that it doesn't require billing information, the payment gateway base outputs an additional setting:

    $form['collect_billing_information'] = [
          '#type' => 'checkbox',
          '#title' => $this->t('Collect billing information'),
          '#description' => $this->t('Before disabling, make sure you are not legally required to collect billing information.'),
          '#default_value' => $this->configuration['collect_billing_information'],
          // Merchants can disable collecting billing information only if the
          // payment gateway indicated that it doesn't require it.
          '#access' => !$this->pluginDefinition['requires_billing_information'],
        ];

    Perhaps we should do the same here?

    The annotation key / attribute value could be "support_free_orders"?

  • 🇮🇱Israel jsacksick

    Sharing here a potential alternative approach... Which could consist in dispatching an event from the isVisible() method, so modules can have a say on whether the payment information pane should be visible.
    This way, thinking of the recurring use case, commerce_recurring could force the payment information pane visible, if the order contains a subscription item?

  • 🇮🇱Israel jsacksick

    Oh wait, this proposal doesn't work at all... For whatever reason, thought the pane was made not visible and had an isVisible() implementation.

  • Pipeline finished with Failed
    28 days ago
    Total: 409s
    #258904
  • 🇳🇴Norway zaporylie

    Re #9

    I am not a big supporter of this approach as I said, the checkbox would conflict with the order total condition that can be set on the Payment Gateway. I believe the compatibility should be set via interface and whether the gateway shows or not can be controlled via condition.

  • Pipeline finished with Success
    28 days ago
    Total: 435s
    #258912
  • 🇳🇴Norway zaporylie

    Changing a bit naming of this issue as while we discussed it, it became obvious that what we're trying to do here is to force payment method collection rather than allow it. Allowing wasn't a correct term either for the current implementation as the compatible payment method was required to complete the checkout. The changes to MR will follow:
    - rename the checkbox accordingly
    - dispatch the event which can modify the value set by the checkbox

  • Pipeline finished with Canceled
    26 days ago
    Total: 74s
    #260109
  • Pipeline finished with Failed
    26 days ago
    Total: 437s
    #260110
  • Pipeline finished with Success
    26 days ago
    Total: 373s
    #260354
  • Pipeline finished with Success
    25 days ago
    Total: 413s
    #261397
  • Pipeline finished with Success
    25 days ago
    Total: 436s
    #261737
  • 🇮🇱Israel jsacksick

    Let's make this happen in 3.0.x instead.

  • Pipeline finished with Success
    19 days ago
    Total: 549s
    #267170
  • Pipeline finished with Failed
    19 days ago
    Total: 678s
    #267181
  • Pipeline finished with Success
    19 days ago
    Total: 727s
    #267180
  • Pipeline finished with Success
    19 days ago
    Total: 1198s
    #267185
  • Pipeline finished with Failed
    19 days ago
    Total: 576s
    #267216
  • Status changed to Needs review 19 days ago
  • 🇳🇴Norway zaporylie

    I closed the previous MR and opened a new one, moving the target branch to 3.0.x

    Some changes since #13
    - Introduced PaymentCheckoutPaneBase which is shared between PaymentInformation and PaymentProcess for the sake of sharing collectBillingProfileOnly()
    - switched from FILTER_PAYMENT_OPTIONS to FILTER_PAYMENT_GATEWAYS and updated the subscriber to affect list of available gateways depending on order balance
    - added tests

  • Pipeline finished with Success
    19 days ago
    Total: 894s
    #267224
  • Pipeline finished with Canceled
    19 days ago
    #267319
  • Pipeline finished with Success
    19 days ago
    Total: 452s
    #267321
  • Status changed to Fixed 19 days ago
  • Pipeline finished with Success
    19 days ago
    Total: 2118s
    #267331
  • 🇳🇴Norway zaporylie

    Added draft CR https://www.drupal.org/node/3470809 for review and publishing

  • Automatically closed - issue fixed for 2 weeks with no activity.

  • Pipeline finished with Failed
    4 days ago
    Total: 471s
    #281253
  • Pipeline finished with Failed
    3 days ago
    #282142
  • Pipeline finished with Failed
    3 days ago
    Total: 438s
    #282566
Production build 0.71.5 2024