mollie / laravel-cashier-mollie

Official Mollie integration for Laravel Cashier
https://www.cashiermollie.com/
MIT License
131 stars 44 forks source link

Automatic Debit Not Working (Unable to Get It Working). Example Code #245

Closed DevTeamNL closed 2 months ago

DevTeamNL commented 3 months ago

My goal is to have customers pay for my service monthly via SEPA incasso "automatische incasso" (the amount must automatically debited monthly). I have authorization for automatic debiting, but I want to do it through Mollie checkout.

I have activated SEPA incasso and iDEAL paymentmethods in Mollie. However, my test code is not working. At checkout, I only get the option to pay via iDEAL for "Verification for Next plus" with an amount of €16.95. But this seems to be a one-time iDEAL payment and not a mandate. Can someone help me and point me in the right direction with example code? I appreciate the assistance, as I'm unable to figure it out on my own.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Models\User;

class SubscriptionController extends Controller
{
    public function subscribe(Request $request)
    {
        $validatedData = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email',
            'phone_number' => 'required|string|max:20',
        ]);

        $user = User::where('email', $validatedData['email'])->first();

        if ($user) {
            $user->update([
                'name' => $validatedData['name'],
                'phone_number' => $validatedData['phone_number'],
            ]);
        } else {
            $user = User::create([
                'name' => $validatedData['name'],
                'email' => $validatedData['email'],
                'phone_number' => $validatedData['phone_number'],
                'password' => ''
            ]);
        }

        Auth::login($user);

        $subscription = $user->newSubscription('main', 'next-plus')->create();
        return $subscription;

//        // Update mollie_customer_id en mollie_mandate_id in de users tabel
//        $user->update([
//            'mollie_customer_id' => $subscription->owner->mollie_customer_id,
//            'mollie_mandate_id' => $subscription->latestMandate()->id,
//        ]);
    }
}

config/cashier_plans.php

<?php

use Laravel\Cashier\Coupon\CouponOrderItemPreprocessor as ProcessCoupons;
use Laravel\Cashier\Order\PersistOrderItemsPreprocessor as PersistOrderItems;
use Laravel\Cashier\Plan\AdvancedIntervalGenerator;

return [
    'defaults' => [
        'first_payment' => config('cashier.first_payment'),
        'order_item_preprocessors' => [
            ProcessCoupons::class,
            PersistOrderItems::class,
        ],
    ],

    'plans' => [
        'next-plus' => [
            'amount' => [
                'value' => '16.95',
                'currency' => 'EUR',
            ],

            'interval' => [
                'generator' => AdvancedIntervalGenerator::class,
                'value' => 1,
                'period' => 'month', /* day, month or year*/
                'monthOverflow' => true,
            ],

            'description' => 'Maandelijkse betaling voor Next+',
        ],
    ],
];

config/cashier.php

<?php

return [
    'webhook_url' => 'webhooks/mollie',
    'aftercare_webhook_url' => 'webhooks/mollie/aftercare',
    'locale' => null,
    'order_number_generator' => [
        'model' => \Laravel\Cashier\Order\OrderNumberGenerator::class,
        'offset' => 0,
    ],
    'first_payment' => [
        'webhook_url' => 'webhooks/mollie/first-payment',
        'method' => ['ideal'],
        'redirect_url' => config('app.url'),
        'amount' => [
            'value' => '0.01',
            'currency' => 'EUR',
        ],
        'description' => 'Verificatie voor Next plus',
    ],
    'update_payment_method' => [
        'redirect_url' => config('app.url'),
        'amount' => [
            'value' => '0.01',
            'currency' => 'EUR',
        ],
        'description' => 'Verificatie voor Next plus',
    ],

];
Naoray commented 3 months ago

Hi @DevTeamNL,

In your cashier.first_payment.method settings, you only have ideal listed. That's why you can't select another payment method at checkout. Add directdebit (Sepa Direct Debit) or banktransfer (Sepa Bank Transfer) to the method options to fix this.

If these aren't the methods you want, use this code to get all available payment methods from Mollie:

Mollie::api()->methods->allAvailable();

Hope this helps! Let me know if you have any questions.

DevTeamNL commented 3 months ago

Hi @Naoray, Thank you for your feedback/help.

I did:

        $methods = Mollie::api()->methods->allAvailable();
        dd($methods);
        exit;

and I see "SEPA Direct Debit" method is activated:

MollieApiClient {#358 ▶}
      +resource: "method"
      +id: "directdebit"
      +description: "SEPA Direct Debit"
      +minimumAmount: {#517 ▶}
      +maximumAmount: {#518 ▶}
      +image: {#519 ▶}
      +issuers: null
      +pricing: null
      +status: "activated"
      +_links: {#521 ▶}
    }

if I change config/cashier.php 'method' => ['directdebit'],

'directdebit' is not accepted:

[2024-05-17 16:19:49] prod.ERROR: [2024-05-17T16:19:49+0000] Error executing API call (422: Unprocessable Entity): The payment method does not support sequence type.

If I leave 'method' => [] empty, then activated payment methods such as iDEAL and Bancontact will show up, but not Direct Debit.

For now I am simulating a payment of 3 euros a day, three times. I think I am on the right track. We'll see if it stops after the third payment.

    'plans' => [
        'next-plus' => [
            'amount' => [
                'value' => '3',
                'currency' => 'EUR',
            ],

            'interval' => [
                'generator' => AdvancedIntervalGenerator::class,
                'value' => 1,
                'period' => 'day', /* day, month or year*/
                'monthOverflow' => true,
            ],
            'times' => '3',
            'description' => 'Dagelijkse betaling voor Next+ abonnement',  // this does not show up anywhere???
        ],
    ],

For testing, after 24 hours have passed, I can manually run a URL with Cashier::run(), and it seems to be working. However, there are bugs. In Mollie's test environment, I observe the same payment reference being debited twice. I cannot reproduce the issue/bug, but in practice, if this occurs, it's not beneficial for the customer.

web.php (routes)

Route::get('cron-job-test', function () {
    $orders = \Laravel\Cashier\Cashier::run();
   echo 'Created '.$orders->count().' orders.';
   exit;
});

Conclusion: I think the first payment (mandate/authorization) must be the same amount as the subscription fee and it must be a payment method like iDeal. It's not a deal-breaker as long as customers are aware and don't incur additional charges. After the payment, customers don't see any information about SEPA Direct Debit.

Already solved questions: Question: Where does the "description" 'Dagelijkse betaling voor Next+ abonnement' appear (key inside config/cashier_plans.php)? Does it show up in the customer's bank statements? Because in the test environment, it doesn't appear anywhere. In the Mollie dashboard, there is a description like "Order 2024-0000-00xx," generated by the method in 'order_number_generator' (OrderNumberGenerator).

Solution: I fixed this by php artisan vendor:publish --tag=cashier-translations and editing 'description' value in lang/vendor/cashier/en/payment.php

Open Questions:

  1. Is there a more efficient way to simulate the process than the one I'm currently using? Waiting for 24 hours each time is not very efficient.

  2. What about VAT/BTW? Where should I include it?

  3. Before I delve into the code, could you please tell me if there's an effective way to revoke a customer's authorization (cancel subscription)? Which table should I modify or is there a command I should run? I couldn't find this information in the documentation.

Have a nice weekend!

Naoray commented 3 months ago

and I see "SEPA Direct Debit" method is activated:

The Mollie::api()->methods->allAvailable() method doesn't return all methods that are activated, but instead returns all methods that are in general available through Mollie (s. https://docs.mollie.com/reference/v2/methods-api/list-all-methods). The naming of the method in the mollie-api-php SDK is a little confusing - I've noted it down to change it in the next major release.

To see if directdebit is active, check Mollie::api()->allActive(). Again, sorry for the confusion!

Besides the already existent confusion, let me add some more 🤷.. You can't use directdebit for your first payment request. All further payments during an active subscription will be handled through a directdebit mandate, but the first payment has to be another method (s. note after point 5 https://docs.mollie.com/payments/recurring#setting-up-the-first-payment)

Note

Not all payment methods support a first payment. When the method parameter is not provided in the API, we take care of this automatically in our Checkout. The following payment methods support a first payment and are thus allowed as a value for the method parameter of a first payment: applepay, bancontact, belfius, creditcard, eps, giropay, ideal, kbc, mybank, paypal, sofort


For now I am simulating a payment of 3 euros a day, three times. I think I am on the right track. We'll see if it stops after the third payment.

What do you mean by 'simulating a payment'?

    'plans' => [
        'next-plus' => [
            //...
            'times' => '3', // <= I don't think this has any consequence if you didn't customize the PlanRepository

            //...
        ],
    ],

For testing, after 24 hours have passed, I can manually run a URL with Cashier::run(), and it seems to be working. However, there are bugs. In Mollie's test environment, I observe the same payment reference being debited twice. I cannot reproduce the issue/bug, but in practice, if this occurs, it's not beneficial for the customer.

The command is not meant to be run via an URL. It should be run through the Laravel Task Scheduler, otherwise you could open the URL twice before the first request has been fully processed and might end up in multiple payments being made, because the first run through might not have updated the DB yet with the new status when the second request was made.

'description' => 'Dagelijkse betaling voor Next+ abonnement', // this does not show up anywhere??

This does appear on the invoice (s. https://www.cashiermollie.com/07-invoices.html on how to generate an invoice). To modify the payment's description that is displayed within the Mollie dashboard, you can either publish the translation files and modify its content (what you already did), or create a custom Order model and override the getDescription() method.


  1. Is there a more efficient way to simulate the process than the one I'm currently using? Waiting for 24 hours each time is not very efficient.

I am not sure what you mean by simulating. If you mean whether there is a better way to test creating a subscription and waiting for the first recurring payment to be executed, you can

  1. create a subscription via $user->newSubscription(..)->create()
  2. complete the full checkout (set payment status to paid)
  3. wait for the webhook to be executed (should be almost immediate)
  4. check for the subscription's OrderItem Model with $user->subscription('subscription_name')->scheduledOrderItem and update the process_at to a datetime in the past.
  5. then run php artisan cashier:run again and the next payment should ve been executed
  1. What about VAT/BTW? Where should I include it?

S. https://www.cashiermollie.com/02-subscriptions.html#subscription-taxes

  1. Before I delve into the code, could you please tell me if there's an effective way to revoke a customer's authorization (cancel subscription)? Which table should I modify or is there a command I should run? I couldn't find this information in the documentation.

$user->subscription('subscription_name')->cancel() (s. https://www.cashiermollie.com/02-subscriptions.html#cancelling-subscriptions).

If you have any other question, feel free to ask them!

alex110414 commented 3 months ago

Hi @DevTeamNL , I think I had the same problem as you. Here is my advice (I did it in ruby):

  1. Create a payment with method="banktransfer":
    payment = {
    customer_id: customer_id,
    amount: {
    value: value,
    currency: currency
    },
    method: "banktransfer",
    webhook_url: "your webhook url",
    redirect_url: "your redirect url",
    }
    Mollie::Payment.create(payment)
  2. Complete the checkout (set payment status to paid)
  3. In your webhook get the payment with the given id:
    payment = Mollie::Payment.get(params[:id])
  4. Get consumerName, consumerAccount and consumerBic from the payment and create a mandate with method="directdebit":
    mandate = Mollie::Customer::Mandate.create(
    customer_id: customer_id,
    method: "directdebit",
    consumer_name: payment.details.consumer_name,
    consumer_account: payment.details.consumer_account,
    consumer_bic: payment.details.consumer_bic,
    )
  5. Create your subscription with the given mandate_id:
    Mollie::Customer::Subscription.create(
    customer_id: customer_id,
    amount: {
    value: value,
    currency: currency
    },
    interval: interval,
    start_date: "now + interval",
    mandate_id: mandate.id,
    )

@Naoray What do you think about this workflow?

sandervanhooft commented 2 months ago

Closing this for now, let me know if it should be reopened.