This course is archived!
This tutorial uses an older version of Symfony of the stripe-php SDK. The majority of the concepts are still valid, though there *are* differences. We've done our best to add notes & comments that describe these changes.
Validate that Coupon in Stripe!
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
The coupon form will submit its code
field right here:
// ... lines 1 - 9 | |
use Symfony\Component\HttpFoundation\Request; | |
class OrderController extends BaseController | |
{ | |
// ... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
// ... lines 86 - 97 | |
} | |
// ... lines 99 - 144 | |
} | |
// ... lines 146 - 147 |
To fetch that POST parameter, add the Request
object as an argument. Then add
$code = $request->request->get('code')
:
// ... lines 1 - 9 | |
use Symfony\Component\HttpFoundation\Request; | |
class OrderController extends BaseController | |
{ | |
// ... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
$code = $request->request->get('code'); | |
// ... lines 87 - 97 | |
} | |
// ... lines 99 - 144 | |
} | |
// ... lines 146 - 147 |
And in case some curious user submits while the form is empty, send back a validation
message with $this->addFlash('error', 'Missing coupon code')
. Redirect to the checkout
page:
// ... lines 1 - 11 | |
class OrderController extends BaseController | |
{ | |
// ... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
$code = $request->request->get('code'); | |
if (!$code) { | |
$this->addFlash('error', 'Missing coupon code!'); | |
return $this->redirectToRoute('order_checkout'); | |
} | |
// ... lines 93 - 97 | |
} | |
// ... lines 99 - 144 | |
} | |
// ... lines 146 - 147 |
Fetching the Coupon Information
Great! At this point, all we need to do is talk to Stripe and ask them:
Hey Stripe! Is this a valid coupon code in your system?. Oh, and if it is, ow much is it for?
Since Coupon
is just an object in Stripe's API, we can fetch it like anything
else. Booya!
As usual, add the API call in StripeClient
. At the bottom, create a new public
function called findCoupon()
with a $code
argument:
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 183 | |
/** | |
* @param $code | |
* @return \Stripe\Coupon | |
*/ | |
public function findCoupon($code) | |
{ | |
// ... line 190 | |
} | |
} |
Then, return \Stripe\Coupon::retrieve()
and pass it the $code
string, which
is the Coupon's primary key in Stripe's API:
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 187 | |
public function findCoupon($code) | |
{ | |
return \Stripe\Coupon::retrieve($code); | |
} | |
} |
Back in OrderController
, add $stripeCoupon = $this->get('stripe_client')
and
then call ->findCoupon($code)
:
// ... lines 1 - 11 | |
class OrderController extends BaseController | |
{ | |
// ... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
$code = $request->request->get('code'); | |
if (!$code) { | |
$this->addFlash('error', 'Missing coupon code!'); | |
return $this->redirectToRoute('order_checkout'); | |
} | |
$stripeCoupon = $this->get('stripe_client') | |
->findCoupon($code); | |
dump($stripeCoupon);die; | |
} | |
// ... lines 99 - 144 | |
} | |
// ... lines 146 - 147 |
If the code is invalid, Stripe will throw an exception. We'll handle that in a few
minutes. But just for now, let's dump($stripeCoupon)
and die
to see what it
looks like.
Ok, refresh, hit "I have a coupon code," fill in our CHEAP_SHEEP
code, and submit!
There it is! In the _values
section where the data hides, the coupon has an id
,
it shows the amount_off
in cents and has a few other things, like duration
, in
case you want to create coupons that are recurring and need to tell the user that
this will be applied multiple times.
Now that we know the coupon is legit, we should add it to our cart. I've already
prepped the cart to be able to store coupons. Just use
$this->get('shopping_cart')
and then call ->setCouponCode()
, passing it the
$code
string and the amount off, in dollars: so $stripeCoupon->amount_off/100
:
// ... lines 1 - 11 | |
class OrderController extends BaseController | |
{ | |
// ... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
// ... lines 86 - 93 | |
$stripeCoupon = $this->get('stripe_client') | |
->findCoupon($code); | |
$this->get('shopping_cart') | |
->setCouponCode($code, $stripeCoupon->amount_off / 100); | |
// ... lines 99 - 102 | |
} | |
// ... lines 104 - 149 | |
} | |
// ... lines 151 - 152 |
The cart will remember - via the session - that the user has this coupon.
We're just about done: add a sweet flash message - "Coupon applied!" - and then redirect back to the checkout page:
// ... lines 1 - 11 | |
class OrderController extends BaseController | |
{ | |
// ... lines 14 - 83 | |
public function addCouponAction(Request $request) | |
{ | |
// ... lines 86 - 96 | |
$this->get('shopping_cart') | |
->setCouponCode($code, $stripeCoupon->amount_off / 100); | |
$this->addFlash('success', 'Coupon applied!'); | |
return $this->redirectToRoute('order_checkout'); | |
} | |
// ... lines 104 - 149 | |
} | |
// ... lines 151 - 152 |
Showing the Code on Checkout
Refresh and re-POST the form! Coupon applied! Except... I don't see any difference: the total is still $99.
Here's why: it's specific to our ShoppingCart
object. In checkout.html.twig
,
we print cart.total
:
// ... lines 1 - 19 | |
{% block body %} | |
<div class="nav-space-checkout"> | |
<div class="container"> | |
<div class="row"> | |
// ... lines 24 - 26 | |
<div class="col-xs-12 col-sm-6"> | |
<table class="table table-bordered"> | |
// ... lines 29 - 49 | |
<tfoot> | |
<tr> | |
<th class="col-xs-6 info">Total</th> | |
<td class="col-xs-3 info checkout-total">${{ cart.total }}</td> | |
</tr> | |
</tfoot> | |
</table> | |
// ... lines 57 - 75 | |
</div> | |
// ... lines 77 - 79 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
I designed the ShoppingCart
class so that the getTotal()
method adds up all
of the product prices plus the subscription total:
// ... lines 1 - 9 | |
class ShoppingCart | |
{ | |
// ... lines 12 - 83 | |
public function getTotal() | |
{ | |
$total = 0; | |
foreach ($this->getProducts() as $product) { | |
$total += $product->getPrice(); | |
} | |
if ($this->getSubscriptionPlan()) { | |
$price = $this->getSubscriptionPlan() | |
->getPrice(); | |
$total += $price; | |
} | |
return $total; | |
} | |
// ... lines 100 - 153 | |
} |
But, this method doesn't subtract the coupon discount. I did this to keep things clean: total is really more like a "sub-total".
But no worries, the method below this - getTotalWithDiscount()
- subtracts the
coupon code:
// ... lines 1 - 9 | |
class ShoppingCart | |
{ | |
// ... lines 12 - 100 | |
public function getTotalWithDiscount() | |
{ | |
return max($this->getTotal() - $this->getCouponCodeValue(), 0); | |
} | |
// ... lines 105 - 153 | |
} |
So back in the template, use cart.totalWithDiscount
:
// ... lines 1 - 19 | |
{% block body %} | |
<div class="nav-space-checkout"> | |
<div class="container"> | |
<div class="row"> | |
// ... lines 24 - 26 | |
<div class="col-xs-12 col-sm-6"> | |
<table class="table table-bordered"> | |
// ... lines 29 - 56 | |
<tfoot> | |
<tr> | |
<th class="col-xs-6 info">Total</th> | |
<td class="col-xs-3 info checkout-total">${{ cart.totalWithDiscount }}</td> | |
</tr> | |
</tfoot> | |
</table> | |
// ... lines 64 - 82 | |
</div> | |
// ... lines 84 - 86 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Ah, now it shows $49.
But, it'll be even clearer if we display the discount in the table. At the bottom
of that table, add a new if statement: if cart.couponCode
and an endif
. Then, copy
the subscription block from above, paste it here, and change the first variable to
cart.couponCode
and the second to cart.couponCodeValue
without the / month
,
unless you want to make all your coupons recurring. Oh, and add "Coupon" in front
of the code:
// ... lines 1 - 19 | |
{% block body %} | |
<div class="nav-space-checkout"> | |
<div class="container"> | |
<div class="row"> | |
// ... lines 24 - 26 | |
<div class="col-xs-12 col-sm-6"> | |
<table class="table table-bordered"> | |
// ... lines 29 - 34 | |
<tbody> | |
// ... lines 36 - 49 | |
{% if cart.couponCode %} | |
<tr> | |
<th class="col-xs-6 checkout-product-name">Coupon {{ cart.couponCode }}</th> | |
<td class="col-xs-3">${{ cart.couponCodeValue }}</td> | |
</tr> | |
{% endif %} | |
</tbody> | |
<tfoot> | |
<tr> | |
<th class="col-xs-6 info">Total</th> | |
<td class="col-xs-3 info checkout-total">${{ cart.totalWithDiscount }}</td> | |
</tr> | |
</tfoot> | |
</table> | |
// ... lines 64 - 82 | |
</div> | |
// ... lines 84 - 86 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
This time, the whole page makes sense! $99 - $50 = $49. It's a miracle!
Now for the easy step: apply the coupon to the user's order at checkout... ya know, so that they actually save $50.
Hey, I was wondering if there's any advantages to using the stripe coupon vs building a coupon entity in symfony? It seems that you have advantages to building a coupon system within symfony as it allows this feature to be customized (user can only use coupon once, coupon tied to specific products only, can attribute coupons to users / "sales people" for commission report generation etc). Is there any advantages to using stripes coupon system? I'm very curious to know *curious pikachu face*