Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
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.

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!

5
Login or Register to join the conversation
Kiuega Avatar

Hello,
If we are using some coupons with percent reduction instead of amount, how are we supposed to do this?

Reply

Hey Kiuega!

I think (but I have a feeling that I'm missing a key part of your question) that it should be the same. The important fact is that Stripe coupons themselves support "amount" and also "percent". So when you create the coupon inside of Stripe, you can choose to create either type. Then, at checkout, the process is the same: apply the coupon & Stripe will handle the rest. We also do this on SymfonyCasts.

But let me know if I'm missing something :).

Cheers!

Reply
Kiuega Avatar
Kiuega Avatar Kiuega | weaverryan | posted 3 years ago | edited

In fact the problem was with our application. When using a coupon with a percentage reduction, then the summary table of our order did not display the correct percentage reduction. He did not support the percentage discount coupon. Only the one with a fixed amount. For that we had to re-work the code

<b>OrderController:</b>
$cart->setCouponCode($code, $stripeCoupon->amount_off /100);
become
$cart->setCouponCode($code, $stripeCoupon);

<b>ShoppingCart:</b>
Add this function :


    public function findCouponAmount(?\Stripe\Coupon $stripeCoupon)
    {
        if(!$stripeCoupon)
        {
            return null;
        }

        if($stripeCoupon->amount_off)
        {
            return $stripeCoupon->amount_off / 100;
        }

        if($stripeCoupon->percent_off)
        {
            $total = $this->getTotal();
            $discount = $total * ($stripeCoupon->percent_off / 100);
            return $discount;
        }
    }

(I need to check if the coupon is null, and in which case, return null, because when the order is placed, we call in <b>ShoppingCart </b>the <b>$this->setCouponCode(null, null);</b> function. And since in this function we will now use the <b>findCouponAmount()</b> function, so we might as well manage the case in it)

And now, the <b>setCouponCode </b>function become


    public function setCouponCode($code, $stripeCoupon)
    {

        $this->session->set(
            self::CART_COUPON_CODE_KEY,
            $code
        );

        $this->session->set(
            self::CART_COUPON_VALUE_KEY,
            $this->findCouponAmount($stripeCoupon)
        );
    }

It seems that it works.
On the other hand, is it normal that when I create a permanent coupon, and that I make subscribe a user by using this code, the coupon is validated. But if I get a new subscription for the user, then even without entering the coupon, I see in Stripe that it is used

However if I watch the session, the coupon is null

Reply
Default user avatar
Default user avatar Blueblazer172 | posted 5 years ago

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

Reply

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!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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
    }
}
userVoice