Applying a Coupon at Checkout

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

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:

... 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():

... 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:

... 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:

... 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:

... 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!

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
        "composer/package-versions-deprecated": "^1.11" // 1.11.99
    },
    "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
    }
}