Buy

There are two ways to use a coupon on checkout: either attach it to the subscription to say "This subscription should have this coupon code" - or - attach it to the customer. They're approximately the same, but we'll attach the coupon to the customer, in part, because the coupon should also work on individual products.

In OrderController, scroll down to the chargeCustomer() method:

152 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 108
private function chargeCustomer($token)
{
$stripeClient = $this->get('stripe_client');
/** @var User $user */
$user = $this->getUser();
if (!$user->getStripeCustomerId()) {
$stripeCustomer = $stripeClient->createCustomer($user, $token);
} else {
$stripeCustomer = $stripeClient->updateCustomerCard($user, $token);
}
// save card details
$this->get('subscription_helper')
->updateCardDetails($user, $stripeCustomer);
$cart = $this->get('shopping_cart');
foreach ($cart->getProducts() as $product) {
$stripeClient->createInvoiceItem(
$product->getPrice() * 100,
$user,
$product->getName()
);
}
if ($cart->getSubscriptionPlan()) {
// a subscription creates an invoice
$stripeSubscription = $stripeClient->createSubscription(
$user,
$cart->getSubscriptionPlan()
);
$this->get('subscription_helper')->addSubscriptionToUser(
$stripeSubscription,
$user
);
} else {
// charge the invoice!
$stripeClient->createInvoice($user, true);
}
}
}
... lines 151 - 152

We know this method: we get or create the Stripe Customer, create InvoiceItems for any products, create the Subscription, and then create an invoice, if needed.

Before adding the invoice items, let's add the coupon to the Customer. So, if $cart->getCouponCodeValue(), then very simply, $stripeCustomer->coupon = $cart->getCouponCode(). Make it official with $customer->save():

157 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 108
private function chargeCustomer($token)
{
... lines 111 - 123
$cart = $this->get('shopping_cart');
if ($cart->getCouponCodeValue()) {
$stripeCustomer->coupon = $cart->getCouponCode();
$stripeCustomer->save();
}
... lines 130 - 153
}
}
... lines 156 - 157

The important thing is that you don't need to change how much you're charging the user: attach the coupon, charge them for the full amount, and let Stripe figure it all out.

I think we should try this out! Use our favorite fake credit card, and Checkout! So far so good!

Find the Customer in Stripe. Yep! There's the order: $49. The invoice tells the whole story: with the sub-total, the discount and the total.

Very, very, nice.

Handling Invalid Coupons

And very easy! So easy, that we have time to add code to handle invalid coupons. Add another item to your cart. Now, try a FAKE coupon code.

Ah! 500 error is no fun. The exception is a \Stripe\Error\InvalidRequest because, basically, the API responds with a 404 status code.

This all falls apart in OrderController on line 95. Hunt that down!

Ah, findCoupon(): surround this beast with a try-catch block for \Stripe\Error\InvalidRequest:

163 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 83
public function addCouponAction(Request $request)
{
... lines 86 - 93
try {
$stripeCoupon = $this->get('stripe_client')
->findCoupon($code);
} catch (\Stripe\Error\InvalidRequest $e) {
... lines 98 - 100
}
... lines 102 - 108
}
... lines 110 - 160
}
... lines 162 - 163

The easiest thing to do is add a flash error message: Invalid Coupon code. Then, redirect back to the checkout page:

163 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 83
public function addCouponAction(Request $request)
{
... lines 86 - 93
try {
$stripeCoupon = $this->get('stripe_client')
->findCoupon($code);
} catch (\Stripe\Error\InvalidRequest $e) {
$this->addFlash('error', 'Invalid coupon code!');
return $this->redirectToRoute('order_checkout');
}
... lines 102 - 108
}
... lines 110 - 160
}
... lines 162 - 163

Refresh that bad coupon! Ok! That's covered!

Expired Coupons

There's just one other situation to handle. In Stripe, find the Coupon section and create a second code. Set the amount to $50, duration "once" and the code: SINGLE_USE. By here's the kicker: set Max redemptions to 1. So, only one customer should be able to use this. There's also a time-sensitive "Redeem by" option.

Quickly, go use the SINGLE_USE code and fill out the form to checkout. This will be the first - and only - allowed "redemption" of this code. When you refresh the Coupon page in Stripe, Redemptions are 1/1.

Now, add another subscription to your cart. If you tried to use the code a second time, our system would allow this. And that makes sense: all we're doing now is looking up the code in Stripe to make sure it exists.

But, if we tried to checkout, Stripe would be pissed: it would not allow us to use the code a second time. Stripe has our back.

But, we should definitely prevent the code from being attached to the cart in the first place. Checkout the Coupon section of Stripe's API docs. Ah, this valid field is the key. This field basically answers this question:

In this moment, can this coupon be used?

Brilliant! Back in OrderController::addCouponAction(), add an if statement: if !$stripeCoupon->valid, then, just like in the catch, add an error flash - "Coupon expired" - and redirect over to the checkout page:

169 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 83
public function addCouponAction(Request $request)
{
... lines 86 - 93
try {
$stripeCoupon = $this->get('stripe_client')
->findCoupon($code);
} catch (\Stripe\Error\InvalidRequest $e) {
$this->addFlash('error', 'Invalid coupon code!');
return $this->redirectToRoute('order_checkout');
}
if (!$stripeCoupon->valid) {
$this->addFlash('error', 'Coupon expired');
return $this->redirectToRoute('order_checkout');
}
... lines 108 - 114
}
... lines 116 - 166
}
... lines 168 - 169

Try it again. Awesome, this time, we get blocked.

If you want to be extra careful, you could add some try-catch logic to your checkout code just to prevent the edge-case where the code becomes invalid between the time of adding it to your cart and checking out. But either way, Stripe will never allow an invalid coupon to be used.

Leave a comment!

  • 2017-03-13 weaverryan

    Blueblazer172 No worries... payment stuff is HARD

    And great question. In the most recent API versions in Stripe, your user would be able to checkout for free in this case, but that's it: they would NOT also receive any account balance for the future (in older versions of Stripe's API, they would checkout for free AND be given account credit for the future).

    1) It's up to you in your code to ultimately decide whether or not you want the coupon to be "accepted" and applied to the order. So, you could definitely use Stripe's API to check the value of the coupon when the user submits it, compare that to the order, and NOT apply it (and give the user a message).

    2) Hmm, I don't think you can allow the user to re-use the rest of the coupon's value later. You CAN edit the customer's account balance (by determining how much of the coupon was unused and then using Stripe's API to add that match to the customer's account balance), but this can be tricky later - your system needs to know to lookup the customer's balance to accurately show the final price of future orders on checkout. It might not be worth it. Another option is to create a "coupon" database table locally on your site. You would then create coupon codes in your system, and these would save in your table (but you would not create them in Stripe, at least at first). Then, on checkout, if the user has applied one of your coupons, use Stripe's API to create the coupon and then apply it immediately. In other words, coupons only exist in your system, until the moment they are applied - then you create + apply them in Stripe all at once. With this setup, you could definitely track how much of a coupon has been used and allow them to re-use the rest later. When they do, you would create a new, smaller coupon in Stripe for the correct value. It's a bit more of a complex setup. So again, it might not be worth it (we actually use this method on KnpU).

    3) I'm not sure if I understand. In this chapter, we DO bind the coupon to the Stripe customer ... so we're already doing that. If you're asking whether or not you can create a coupon that can only be redeemed by a user in your database, then totally! In this tutorial, we don't keep a record of the coupons in our database. But, in real-life, you might want to do this - e.g. a coupons table. In this case, I would add a user_id to the coupons table, and when you create a coupon, make sure you associate it with whatever user you want. In a really nice setup, you might have an admin interface in your app for creating coupons. On that interface, perhaps you can select the user in your system who this coupon is for. Then, when you submit, it would create the record in your local coupon table, and also create the Coupon object in stripe. In checkout, you could check to make sure (in your database) that the coupon is being used by the person it is assigned to.

    Cheers!

  • 2017-03-10 Blueblazer172

    Jo sorry for that much questions :P

    But what if the user uses a coupon and the price in his cart is lower than the coupon's price.

    1. Is there a way to prevent this? and only set it up if the actual price is higher than the coupon price?
    2. Is there a way to set the coupons price to the value of the left money there if the cart price is lower and let the user reuse the coupon if he wants to use it again ?
    3. Can I bind a coupon to a defined user ?

    Thanks :)