Closed RobertBoes closed 4 years ago
@RobertBoes that's unfortunate, will have to look into this later this week.
It may help to understand in debugging that using the coupon is actually a two-step process:
Can you tell me what subscription it is? I.e. trial subscription with payment upfront?
It's a subscription with a generic trial, so the user doesn't have a subscription yet and no payments have been made yet, i.e. the first_payment (1 euro) is not used. I'm using the example code that's provided in the readme, but I've added ->withCoupon('welcome')
.
I would assume that when creating a new subscription with a coupon, the coupon would be applied when making the first payment. I don't know if this doesn't work because of the errors, or because the coupon is never applied but only on subsequent payments.
This is basically the full code I'm using:
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
class CreateSubscriptionController extends Controller
{
/**
* @param string $plan
* @return \Illuminate\Http\RedirectResponse
*/
public function __invoke(string $plan)
{
$user = Auth::user();
$name = ucfirst($plan) . ' membership';
if(!$user->subscribed($name, $plan)) {
$result = $user->newSubscription($name, $plan)->withCoupon('welcome')->create();
if(is_a($result, RedirectResponse::class)) {
return $result; // Redirect to Mollie checkout
}
return back()->with('status', 'Welcome to the ' . $plan . ' plan');
}
return back()->with('status', 'You are already on the ' . $plan . ' plan');
}
}
(FYI, your code looks like you're starting a normal subscription, not a generic one, but that's beside the point here 😄)
Ok. So there are a few things going on here that need attention. Let me share my thoughts here.
Right now, the business logic is as such that the coupon should not be applicable to a trial.
But...
a) it should also be possible to redeem in trial mode.
b) if applicable, an InvalidCouponException
should be thrown before the customer is redirected to the checkout. This probably needs to happen in the SubscriptionBuilder
s.
c) an exception while handling the coupon after payment should not kill the rest of the flow. This relates to payment actions and how to (gracefully) handle exceptions.
@robertboes can you share the metadata from the payment (you can find this in the Mollie dashboard)?
Sure, here it is:
{
"owner": {
"type": "App\\User",
"id": 2
},
"actions": [
{
"handler": "Laravel\\Cashier\\FirstPayment\\Actions\\StartSubscription",
"description": "Monthly payment",
"subtotal": {
"currency": "EUR",
"value": "10.00"
},
"plan": "example-1",
"name": "main",
"quantity": 1,
"coupon": "welcome"
}
]
}
I've done some more testing and it seems like the preprocessors are not applied on the first payment, is that correct?
When I create a subscription with a trial, like this:
$result = $user
->newSubscription($name, $plan)
->trialUntil(Carbon::now()->addMinutes(30))
->withCoupon('welcome')
->create();
The first_payment
is used, so the first payment is the amount that specified there (which I've set to 0.01). After I run php artisan cashier:run
the subsequent payment is processed, which is then charged as 10.00 minus the coupon discount of 5.00, so the total that's paid is 5.00.
However, when I run this without a trial and with a coupon, the amount that needs to be paid for the first payment is 10.00 instead of 5.00 (10.00 plan - 5.00 coupon). So the first payment ignores the preprocessors.
This might be related to the error I get within the webhook when using a coupon upon creating the subscription, but I'm not sure.
Thanks for following up. I have been recreating this issue this afternoon, and can confirm your suspicions about the first payment not having applied the discount yet.
// FirstPaymentSubscriptionBuilderTest.php
/** @test */
public function buildsPayloadWithCouponNoTrial()
{
$this->withMockedCouponRepository();
$this->assertSame(0, RedeemedCoupon::count());
$this->assertSame(0, AppliedCoupon::count());
$builder = $this->getBuilder()
->withCoupon('test-coupon'); // 5 EUR off
$builder->create();
// Coupons will not be handled before payment is completed via checkout
$this->assertSame(0, RedeemedCoupon::count());
$this->assertSame(0, AppliedCoupon::count());
$payload = $builder->getMandatePaymentBuilder()->getMolliePayload();
$this->assertSame('EUR', $payload['amount']['currency']);
$this->assertSame('7.00', $payload['amount']['value']); // <-- fails here (12.00 instead of 7.00)
}
and with trial is same problem
$this->assertSame('7.00', $payload['amount']['value']); // <-- fails here (0.05 instead of 7.00)
I think that's correct behaviour actually @ciungulete
I'm digging in today/tomorrow in the coupon logic. Complex stuff, simplifying some code while going over it.
Generally speaking (there may be some exceptions here as I noted above, but let's start here):
✅ Mandate available, no trial: validate, redeem on creating the subscription. Apply coupon during processing the scheduled payment ✅ Mandate available, with trial: validate, redeem coupon on creating the subscription. Apply coupon during processing first scheduled normal payment
Through checkout, no trial
Before checkout:
After checkout:
Through checkout, with trial
Before checkout:
After checkout:
Still at it, hope to release an improved version later this week.
Any update on the progress? We really want to start implementing this, but we're depending on preprocessors to work for the very first payment.
Hi @RobertBoes ,
Good news: I'm preparing the PR right now.
The issue was more complex than I expected, so I had to sit on it for a bit before moving forward. Sorry for that.
I'll open a separate issue for documenting the coupon logic.
Hi @sandervanhooft ,
Thanks for your work! I tried your fix and there seems to be an issue. When I apply a coupon to a new subscription, the following metadata is sent to Mollie:
{
"owner": {
"type": "App\\Company",
"id": 1
},
"actions": [
{
"handler": "Laravel\\Cashier\\FirstPayment\\Actions\\StartSubscription",
"description": "Pro abonnement",
"subtotal": {
"currency": "EUR",
"value": "19.00"
},
"taxPercentage": "21.0000",
"plan": "pro",
"name": "default",
"quantity": 1,
"coupon": "ptquku"
},
null
]
}
The last item in the array is null
, which causes the following error when Mollie does its callback:
Trying to get property 'handler' of non-object {"exception":"[object] (ErrorException(code: 0): Trying to get property 'handler' of non-object at \\vendor\\laravel\\cashier-mollie\\src\\FirstPayment\\FirstPaymentHandler.php:84)
Thanks for the super fast response!
I've just released v1.2.1
to fix this issue, can you give it a spin?
Yes, that works! Is it correct that coupons are not mentioned on invoices?
I've tried to create my own coupon, but the documentation is very minimal, so I've basically copied the
FixedDiscountHandler
and changed a few things.Now my own coupon didn't work, so I applied the default coupon (with the default plan etc.), and I think a lot of things are not working correctly:
Trying to get property 'currency' of non-object at ...\\vendor\\laravel\\cashier-mollie\\src\\Order\\Order.php:75
The code I used is the same as in the "documentation" / readme, except I changed the part where I make a new subscription, like this:
This is the complete error log:
I think the subscriptions are not saved to my own DB because of this error, this kind of worries me, since we have no control over this and it's hard to debug if a user actually bought a product.
The coupon that's not applied also seems very strange, it might be applied during the next payment, but that's a weird flow, right?