Buy

The coupon form will submit its code field right here:

147 lines src/AppBundle/Controller/OrderController.php
... 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'):

147 lines src/AppBundle/Controller/OrderController.php
... 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:

147 lines src/AppBundle/Controller/OrderController.php
... 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:

193 lines src/AppBundle/StripeClient.php
... 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:

193 lines src/AppBundle/StripeClient.php
... 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):

147 lines src/AppBundle/Controller/OrderController.php
... 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:

152 lines src/AppBundle/Controller/OrderController.php
... 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:

152 lines src/AppBundle/Controller/OrderController.php
... 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:

84 lines app/Resources/views/order/checkout.html.twig
... 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:

155 lines src/AppBundle/Store/ShoppingCart.php
... 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:

155 lines src/AppBundle/Store/ShoppingCart.php
... 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:

91 lines app/Resources/views/order/checkout.html.twig
... 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:

91 lines app/Resources/views/order/checkout.html.twig
... 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!

  • 2017-03-10 Diego Aguiar

    Actually you can choose whatever name you want, is just for your convenience, if you are lazy (I mean wise) like me, you could name those messages with the same name as bootstrap class and loop through them like this:


    {% for type, messages in app.session.flashBag.all %}
    {% for message in messages %}
    <div class="alert-{{ type }}">
    {{ message|trans }}
    </div>
    {% endfor %}
    {% endfor %}

    Then you can add more logic if you have other kind of messages

  • 2017-03-10 Blueblazer172

    thanks for replying. im familliar with the bootstrap alerts :)

    but what i mean is this:

     $this->addFlash('error', $error);


    to something like this:

     $this->addFlash('warning', $error);

    is that possible ?

  • 2017-03-10 Diego Aguiar

    Hey Blueblazer172

    Flash messages works as bags, whenever you add a new flash message it's stored to the name you chose, so then you can loop through them and do what you need, in your case, rendering an element with a bootstrap class "alert-warning", you can check all alert types here:
    http://getbootstrap.com/com...

    Have a nice day!

  • 2017-03-10 Blueblazer172

    is there a warning attribute for an addFlash function? i only see success and error :) would be nice if i can get it like those 3 from bootstrap :)