laravel / cashier-mollie

MIT License
375 stars 63 forks source link

Updating the payment method #205

Closed jponsen closed 4 years ago

jponsen commented 4 years ago

Hi,

I'm currently working on a project where we need to have the possibility to swap the issuer of the payment. Example:

Customer has registered and used iDeal with Rabobank (issuer) for first payment, and wants to switch to ING after one month.

Is there any way to switch this using a built in function? When selecting a new issuer and calling the newSubscription method, a Subscription is returned instead of a RedirectToCheckoutResponse (which is needed to retrieve a new mandate).

Any help would be appreciated!

Thanks.

sandervanhooft commented 4 years ago

There's no simple call for this yet. But there are two ways to accomplish the use case you describe here. (Note that an iDEAL payment results in a directdebit mandate.)

If you want to verify the new card through Mollie's checkout and are ok with a minimum payment amount, you can use the FirstPaymentBuilder with either an AddGenericOrderItem action or an AddBalance action.

Alternatively you can create a new direct debit mandate for the customer using the Mollie mandates API. For directdebit this can even be done without having the customer go through checkout. Make sure to update the new mandateId on the billable model.

jponsen commented 4 years ago

@sandervanhooft Thanks for your detailled reply. I just noticed I didn't mention that we also need the possibility to change between directdebit and creditcard. What is the way to go for this?

Is there any way using this package (or another way) to retrieve a new mandate for the other payment method? Since we don't know the Users credit card details I should probably redirect the user to the Mollie checkout screen. But what if the user already have a subscription they paid for (and are we able to lower the first payment price to €0.01 if this step needs to be done)?

sandervanhooft commented 4 years ago

It's a common use case. But it can also get a bit complicated as you are hinting here 😄 .

Adding a default method for this is on the roadmap. The Spark-Mollie launch (May 28th) comes first though - and I'm still recovering from what probably was covid-19.

Personally I'm always redirecting the customer through Mollie's checkout to be sure that the new card is verified. In many cases it is more customer-friendly to add the minimum payment amount to the customer's balance.

Here's an excerpt of one of my implementations. If it's helpful to you, consider converting it into a pull request.

<?php
use Laravel\Cashier\FirstPayment\Actions\AddBalance;
use Laravel\Cashier\FirstPayment\Actions\AddGenericOrderItem;
use Laravel\Cashier\FirstPayment\FirstPaymentBuilder;
use Money\Money;

class UpdateMolliePaymentMethod
{
    protected $shouldAddFeeToBalance = false;

    public function handle($billable, array $data)
    {
        $payment = (new FirstPaymentBuilder($billable, $this->getFirstPaymentOptions($billable)))
            ->setRedirectUrl('/payment-method')
            ->inOrderTo($this->getPaymentActions($billable, $data))
            ->create();

        $payment->redirectUrl = url('/redirects/mollie/update-payment-method/' . $payment->id);
        $payment->update();

        return response([
            'data' => [
                'checkoutUrl' => $payment->getCheckoutUrl(),
            ],
        ]);
    }

    protected function getFirstPaymentOptions($billable)
    {
        return [
            // set any options specific to your use case here
        ];
    }

    protected function getPaymentActions($billable, $data)
    {
        if($this->shouldAddFeeToBalance) {
            return $this->getAddToBalanceActions($billable, $data);
        }

        // VAT is involved if the payment is not used for adding balance
        $subtotal = $this->subtotalForTotalIncludingTax(
            mollie_array_to_money(config('cashier.first_payment.amount')),
            $billable->taxPercentage() * 0.01
        );

        return [ new AddGenericOrderItem($billable, $subtotal, __("Payment method updated")) ];
    }

    protected function getAddToBalanceActions($billable, $data)
    {
        return [
            new AddBalance(
                $billable,
                mollie_array_to_money(config('cashier.first_payment.amount')),
                __("Payment method updated")
            )
        ];
    }

    protected function subtotalForTotalIncludingTax(Money $total, float $taxPercentage)
    {
        $vat = $total->divide(1 + $taxPercentage)->multiply($taxPercentage);

        return $total->subtract($vat);
    }
}
sandervanhooft commented 4 years ago

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

blankflo commented 3 years ago

Hi , i am newbie for that package but i enjoy it , and for my project i need to update payment method . Can someone help me to do it pls , i dont understand how you can do it with the class quoted above . It will be vey nice , thank you in advance !

sandervanhooft commented 3 years ago

@blankflo At what point do you get stuck?

blankflo commented 3 years ago
<?php
use Laravel\Cashier\FirstPayment\Actions\AddBalance;
use Laravel\Cashier\FirstPayment\Actions\AddGenericOrderItem;
use Laravel\Cashier\FirstPayment\FirstPaymentBuilder;
use Money\Money;

class UpdateMolliePaymentMethod
{
    protected $shouldAddFeeToBalance = false;

    public function handle($billable, array $data)
    {
        $payment = (new FirstPaymentBuilder($billable, $this->getFirstPaymentOptions($billable)))
            ->setRedirectUrl('/payment-method')
            ->inOrderTo($this->getPaymentActions($billable, $data))
            ->create();

        $payment->redirectUrl = url('/redirects/mollie/update-payment-method/' . $payment->id);
        $payment->update();

        return response([
            'data' => [
                'checkoutUrl' => $payment->getCheckoutUrl(),
            ],
        ]);
    }

    protected function getFirstPaymentOptions($billable)
    {
        return [
            // set any options specific to your use case here
        ];
    }

    protected function getPaymentActions($billable, $data)
    {
        if($this->shouldAddFeeToBalance) {
            return $this->getAddToBalanceActions($billable, $data);
        }

        // VAT is involved if the payment is not used for adding balance
        $subtotal = $this->subtotalForTotalIncludingTax(
            mollie_array_to_money(config('cashier.first_payment.amount')),
            $billable->taxPercentage() * 0.01
        );

        return [ new AddGenericOrderItem($billable, $subtotal, __("Payment method updated")) ];
    }

    protected function getAddToBalanceActions($billable, $data)
    {
        return [
            new AddBalance(
                $billable,
                mollie_array_to_money(config('cashier.first_payment.amount')),
                __("Payment method updated")
            )
        ];
    }

    protected function subtotalForTotalIncludingTax(Money $total, float $taxPercentage)
    {
        $vat = $total->divide(1 + $taxPercentage)->multiply($taxPercentage);

        return $total->subtract($vat);
    }
}

I dont understand how to update payment method with you're implementation . I am sorry but i'am a beginner . I have "billing page" and the customers can change their payment method with a button . My first idea was to revoke the mandate with mollie api and then make a first payment but the customer is still on active subscriptions , and the funtion return a subscription .

Btw , awesome work for the package !

sandervanhooft commented 3 years ago

The above handle method is for a SPA (derived from Spark in fact).

If you're using Blade, you could do something like this:

public function handle($billable, array $data)
    {
        $payment = (new FirstPaymentBuilder($billable, $this->getFirstPaymentOptions($billable)))
            ->setRedirectUrl('/payment-method')
            ->inOrderTo($this->getPaymentActions($billable, $data))
            ->create();

        $payment->redirectUrl = url('/redirects/mollie/update-payment-method/' . $payment->id);
        $payment->update();

        return redirect($payment->getCheckoutUrl());
    }

~Make sure to add the /payment-method route and a matching webhook controller that takes care of updating the mandate id on the billable (user) model in case of a successful payment.~

blankflo commented 3 years ago

Ok thank you . So in fact , I have to do a controller which can grab new mandate ID and update it on the billable . Then because mollie is asynchronous i have to do a route to redirect when the payment is succesful ? How can i call the function above ? in my webhook Controller ?

sandervanhooft commented 3 years ago

Sorry, I made a mistake above. No need to set up another route / webhook.

Call the handle method described above when your "update" button gets clicked, the customer will then get redirected to Mollie's checkout. Once the payment gets status "paid" the mandateId will automatically get updated on the billable model.

blankflo commented 3 years ago

Ok it's more clear now . Last questions , i call the handle function without parameters because they are define by default in the construct of TheFirstPaymentBuilder class ? Thank you for you're help and the work . I will test the solution now . I will publish the solution in the issue who i have created for everyone see it . Have a nice day .

blankflo commented 3 years ago
<?php

namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use  Hash;
use App\Models\User;
use Laravel\Cashier\FirstPayment\Actions\AddBalance;
use Laravel\Cashier\FirstPayment\Actions\AddGenericOrderItem;
use Laravel\Cashier\FirstPayment\FirstPaymentBuilder;
use Money\Money;

class UpdateMolliePayment extends Controller{

    public function handle()
    {
        $billable = Auth::user();

        $payment = (new FirstPaymentBuilder($billable))
            ->setRedirectUrl('/billing')
            ->inOrderTo($this->getAddToBalanceActions($billable))

            ->create();

         $payment->update();

        return redirect($payment->getCheckoutUrl());
    }

    protected function getAddToBalanceActions($billable)
    {
        return [
            new AddBalance(
                $billable,
                mollie_array_to_money(config('cashier.first_payment.amount')),
                __("Payment method updated")
            )
        ];
}
}

I cant test in live for now but it seems working , what do you think about it @sandervanhooft ?

sandervanhooft commented 3 years ago

Looks good to me!

blankflo commented 3 years ago

Thank you very much for your help ! If it's works in live mode , i will say it here . Have a good evening .