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.

Start your All-Access Pass
Buy just this tutorial for $12.00

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.

Leave a comment!

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.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9, <7.4",
        "symfony/symfony": "3.1.*", // v3.1.10
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.8
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.2
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.3.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "~2.0.1", // v2.0.1
        "stof/doctrine-extensions-bundle": "^1.2", // v1.2.2
        "stripe/stripe-php": "^3.15", // v3.23.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.2.1
        "phpunit/phpunit": "^5.5", // 5.7.20
        "twig/twig": "^1.24.1" // v1.35.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.4
        "symfony/phpunit-bridge": "^3.0", // v3.3.0
        "hautelook/alice-bundle": "^1.3", // v1.4.1
        "doctrine/data-fixtures": "^1.2" // v1.2.2
    }
}