Buy

Let's talk about something fun: coupon codes. When a user checks out, I want them to be able to add a coupon code. Fortunately Stripe totally supports this, and that makes our job easier.

In fact, in the Stripe dashboard, there's a Coupon section. Create a new coupon: you can choose either a percent off or an amount off. To make things simple, I'm only going to support coupons that are for a specific amount off.

Create one that saves us $50. Oh, and in case this is used on an order with a subscription, you can set the duration to once, multi-month or forever.

Then, give the code a creative, unique code: like CHEAP_SHEEP. Oh, and the coupon codes are case sensitive.

Tip

I'm choosing to create my coupons through Stripe's admin interface. If you want an admin section that does this on your site, that's totally possible! You can create new Coupons through Stripe's API.

Adding a Coupon During Checkout

Back on our site, before we get into any Stripe integration, we need to add a spot for adding coupons during checkout.

Open the template: order/checkout.html.twig. Below the cart table, add a button, give it some styling classes and a js-show-code-form class. Say, "I have a coupon code":

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">
... lines 28 - 57
<button class="btn btn-xs btn-link pull-right js-show-code-form">
I have a coupon code
</button>
... lines 61 - 75
</div>
... lines 77 - 79
</div>
</div>
</div>
{% endblock %}

Instead of adding this form by hand, open your tutorial/ directory: this is included in the code download. Open coupon-form.twig, copy its code, then paste it below the button:

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">
... lines 28 - 57
<button class="btn btn-xs btn-link pull-right js-show-code-form">
I have a coupon code
</button>
<div class="js-code-form" style="display: none;">
<form action="" method="POST" class="form-inline">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-terminal"></i>
</span>
<input type="text" name="code" autocomplete="off" class="form-control" placeholder="Coupon Code"/>
</div>
<button type="submit" class="btn btn-primary">Add</button>
</div>
</form>
</div>
</div>
... lines 77 - 79
</div>
</div>
</div>
{% endblock %}

This new div is hidden by default and has a js-code-form class that we'll use soon via JavaScript. And, it has just one field named code.

Copy the js-show-code-form class and scroll up to the javascripts block. Add a new document.ready() function:

84 lines app/Resources/views/order/checkout.html.twig
... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script>
jQuery(document).ready(function() {
... lines 11 - 15
});
</script>
{% endblock %}
... lines 19 - 84

Inside, find the .js-show-code-form element and on click, add a callback. Start with our favorite e.preventDefault():

84 lines app/Resources/views/order/checkout.html.twig
... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script>
jQuery(document).ready(function() {
$('.js-show-code-form').on('click', function(e) {
e.preventDefault();
... lines 13 - 14
})
});
</script>
{% endblock %}
... lines 19 - 84

Then, scroll down to the form, copy the js-code-form class, use jQuery to select this, and... drumroll... show it!

84 lines app/Resources/views/order/checkout.html.twig
... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script>
jQuery(document).ready(function() {
$('.js-show-code-form').on('click', function(e) {
e.preventDefault();
$('.js-code-form').show();
})
});
</script>
{% endblock %}
... lines 19 - 84

Cool! Now when you refresh, we have a new link that shows the form.

Submitting the Coupon Form

So let's move to phase two: when we hit "Add", this should submit to a new endpoint that validates the code in Stripe and attaches it to our user's cart.

To create the new endpoint, open OrderController. Near the bottom add a new public function addCouponAction() with @Route("/checkout/coupon"). Name it order_add_coupon. And to be extra-hipster, add @Method("POST") to guarantee that you can only POST to this:

147 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 9
use Symfony\Component\HttpFoundation\Request;
class OrderController extends BaseController
{
... lines 14 - 79
/**
* @Route("/checkout/coupon", name="order_add_coupon")
* @Method("POST")
*/
public function addCouponAction(Request $request)
{
... lines 86 - 97
}
... lines 99 - 144
}
... lines 146 - 147

Cool! Copy the route name, then find the coupon form in the checkout template. Update the form's action: add path() and then paste the route name:

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">
... lines 28 - 61
<div class="js-code-form" style="display: none;">
<form action="{{ path('order_add_coupon') }}" method="POST" class="form-inline">
... lines 64 - 73
</form>
</div>
</div>
... lines 77 - 79
</div>
</div>
</div>
{% endblock %}

Next, we'll read the submitted code and check with Stripe to make sure it's real, and not just someone trying to guess clever coupon codes. Come on, we've all tried it before.

Leave a comment!

  • 2017-04-04 Diego Aguiar

    Hmm, looks like you are mixing some things:
    - You shouldn't need to pass the shopping cart to the template, only the products
    - Instead of asking for product ID to the shopping cart, you can ask it directly to the product itself
    - And if the user hasn't selected anything, then he can't remove anything :)

    I hope this makes sense to you :)

    Have a nice day!

  • 2017-04-04 Blueblazer172

    yeah my problem is passing the variable to the template, but how can i if the user hasn't selected something.

    i tried like this in ordercontroller.php:


    /**
    * @Route("/checkout", name="order_checkout", schemes={"%secure_channel%"})
    * @Security("is_granted('ROLE_USER')")
    */
    public function checkoutAction(Request $request, Product $product)
    {
    if (!$products = $this->get('shopping_cart')->getProducts()) {
    $this->addFlash('warning', 'No Products in Cart');
    return $this->redirectToRoute('homepage');
    }

    $error = false;
    if ($request->isMethod('POST')) {
    $token = $request->request->get('stripeToken');

    try {
    $this->chargeCustomer($token);
    } catch (\Stripe\Error\Card $e) {
    $error = 'There was a problem charging your card: '.$e->getMessage();
    }

    if (!$error) {
    $this->em = $this->get('fos_user.user_manager');
    $role = $this->getUser()->addRole('ROLE_BUYER_USER');
    $this->em->updateUser($role);

    $this->get('shopping_cart')->emptyCart();
    $this->addFlash('success', 'Yay! Order fully completed!');

    return $this->redirectToRoute('homepage');
    }
    }

    return $this->render('order/checkout.html.twig', array(
    'products' => $products,
    'cart' => $this->get('shopping_cart'),
    'stripe_public_key' => $this->getParameter('stripe_public_key'),
    'error' => $error,
    'product' => $this->get('shopping_cart')->getProductId($product)
    ));

    }

    and the getProductId() funtion in the shoppingcart.php:

    public function getProductId(Product $product)
    {
    return $product->getId();
    }

    but then i get this error:

    Unable to guess how to get a Doctrine instance from the request information for parameter "product".

  • 2017-03-29 Diego Aguiar

    Hey Blueblazer172
    As Victor said, you might have forgotten to pass "product" variable to the template

    Have a nice day!

  • 2017-03-29 Victor Bocharsky

    Hey Blueblazer172 ,

    Please, ensure you're passing "product" variable in all places where you render "order/checkout.html.twig" template - I bet you just missed it somewhere.

    Cheers!

  • 2017-03-29 Blueblazer172

    but now i get this nasty error: http://imgur.com/a/hU9Kx why does it not recognice the product variable, because it is existing?

  • 2017-03-29 Blueblazer172

    thanks you are awasome :)

  • 2017-03-28 Diego Aguiar

    Hey Blueblazer,
    Sorry! I forgot about that method :p
    Your implementation looks fine to me, but I think this way may be faster


    public function removeProduct(Product $product){
    $products = $this->getProducts();

    if(in_array($product, $products)){
    unset($products[$product->getId()]);
    $this->updateProducts($products);
    }
    }

    Products are only updated if the item was found, if it wasn't found you might want to throw an exception because it shouldn't happen

    Cheers!

  • 2017-03-28 Blueblazer172

    currently my 2 functions in the shoppingcart look like this:


    public function addProduct(Product $product)
    {
    $products = $this->getProducts();

    if (!in_array($product, $products)) {
    $products[] = $product;
    }

    $this->updateProducts($products);
    }

    public function removeProduct(Product $product)
    {
    $products = $this->getProducts();
    $id = $product->getId();

    if (($key = array_search($id, $products)) !== false) {
    unset($products[$key]);
    }

    $this->updateProducts($products);
    }

  • 2017-03-28 Blueblazer172

    thanks :)
    but how should the removeProduct function look like? :P

  • 2017-03-28 Diego Aguiar

    Hey Blueblazer!
    Nice idea, definitely an user will find it helpful

    You need two things here, one in your template for sending an AJAX to the server, and a Route in your controller, for handling that request

    If you are using Jquery you can use $.ajax() method for seding that request, like this:


    $.ajax({
    url: "{{ path('order_remove_product', {id: product.id}) }}",
    method: "DELETE"
    })
    .done(function( data ) {
    //data variable contains the response from the server
    //logic for showing a successful message to the user
    });


    Yep, you can use twig inside javascript for simplifying some things, and you only have to adjust that path and product id

    Controller:


    /**
    * @Route("/cart/product/{id}", name="order_remove_product")
    * @Method("DELETE")
    */
    public function removeProductAction(Product $product)
    {
    $this->get('shopping_cart')
    ->removeProduct($product);

    return new JsonResponse('Product removed!');
    }

    You can read more about Jquery AJAX here:
    http://api.jquery.com/jquer...

    Cheers!

  • 2017-03-27 Blueblazer172

    Yo :)
    It would be really handy if a user could remove a product from the cart :)
    my cart looks like this: https://imgur.com/a/H6Y6l

    now when someone clicks on the x the product should be removed from the cart and the cart should be updated.
    my template looks like this:
    https://imgur.com/a/H6Y6l

    and i started the javascript like this:


    $('.removeproduct').on('click', function(e) {
    e.preventDefault();
    //here is something missing :P
    })

    i dont know how to send a request to the server and then remove the product or subscription.
    a hint for me?
    Thanks for anything :)