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.

Stripe Invoices

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 $9.00

The first two important objects in Stripe are Charge and Customer.

Let's talk about the third important object: Invoice. Here's the idea: right now, we simply charge the Customer. But instead of doing that, we could add invoice items to the Customer, create an Invoice for those items, and then pay that Invoice.

To the user, this feels the same. But in Stripe, instead of having a charge, you will have an Invoice full of invoice items, and a charge to pay that invoice. Why do we care? Well first, it let's you have detailed line-items - like two separate items if our customer orders 2 products.

And second, invoices are central to handling subscriptions. In fact, you'll find the Invoice API documentation under the subscription area. But, it can be used for any charges.

Creating & Paying the Invoice

Let's hook this up. First, instead of creating a Stripe Charge, create a Stripe InvoiceItem:

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
if ($request->isMethod('POST')) {
... lines 37 - 59
\Stripe\InvoiceItem::create(array(
"amount" => $this->get('shopping_cart')->getTotal() * 100,
"currency" => "usd",
"customer" => $user->getStripeCustomerId(),
"description" => "First test charge!"
));
... lines 66 - 75
}
... lines 77 - 83
}
}

But all the data is the same.

Below that, add $invoice = \Stripe\Invoice::create() and pass that an array with customer set to $user->getStripeCustomerId():

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
if ($request->isMethod('POST')) {
... lines 37 - 59
\Stripe\InvoiceItem::create(array(
"amount" => $this->get('shopping_cart')->getTotal() * 100,
"currency" => "usd",
"customer" => $user->getStripeCustomerId(),
"description" => "First test charge!"
));
$invoice = \Stripe\Invoice::create(array(
"customer" => $user->getStripeCustomerId()
));
... lines 69 - 75
}
... lines 77 - 83
}
}

Finally, add $invoice->pay():

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
if ($request->isMethod('POST')) {
... lines 37 - 65
$invoice = \Stripe\Invoice::create(array(
"customer" => $user->getStripeCustomerId()
));
// guarantee it charges *right* now
$invoice->pay();
... lines 71 - 75
}
... lines 77 - 83
}
}

Let's break this down. The first part creates an InvoiceItem in Stripe, but nothing is charged yet. Then, when you create an Invoice, Stripe looks for all unpaid invoice items and attaches them to that Invoice. The last line charges the customer to pay that invoice's balance.

Usually, when you create an Invoice, Stripe will charge the customer immediately. But, if you have web hooks setup - something we'll talk about in the second course - that will delay charging the user by 1 hour. Calling ->pay() guarantees that this happens right now.

Ok, go back and try this out. Find a wonderful and high-quality product, and add it to the cart. Checkout using your favorite fake credit card and fake information.

Looks like it worked! And since this user already is a Stripe customer, refresh that customer's page in Stripe. Check this out! We have two payments and we can see the Invoice. If you click that, the Invoice has 1 line item and a related Charge object.

Tip

If all charges belong to an invoice, you can use Stripe's API to retrieve your customer's past invoices and render them as a receipt.

One InvoiceItem per Product

This now gives us more flexibility. Since sheep love to shop, they'll often buy multiple products. In fact, let's go buy some shears, and some Sheerly Conditioned. If we checked out right now, this would show up as one giant line item for $106.00 on the Invoice. We can do better than that.

In OrderController, around the InvoiceItem part, add a foreach, over $this->get('shopping_cart')->getProducts() as $product. In other words, let's loop over all the actual products in our cart and create a separate InvoiceItem for each. All we need to do is change the amount to be: $product->getPrice() * 100. We can even improve the description: set it to $product->getName():

... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 31
public function checkoutAction(Request $request)
{
... lines 34 - 35
if ($request->isMethod('POST')) {
... lines 37 - 59
foreach ($this->get('shopping_cart')->getProducts() as $product) {
\Stripe\InvoiceItem::create(array(
"amount" => $product->getPrice() * 100,
"currency" => "usd",
"customer" => $user->getStripeCustomerId(),
"description" => $product->getName()
));
}
... lines 68 - 77
}
... lines 79 - 85
}
}

Now, if we eventually send the user a receipt, it's going to be very easy to look on this Stripe invoice and see exactly what we charged them for.

Test time! Put in our awesome fake information, hit ENTER... and no errors.

In Stripe, click back to the customer page and find the new invoice - for $106. Click on that. Yes! 2 crystal clear invoice line items.

So yes, you can just charge customers. But if you create an Invoice with detailed line items, you're going to have a much better accounting system in the long run.

Leave a comment!

5
Login or Register to join the conversation
Default user avatar
Default user avatar Thierno Diop | posted 5 years ago

hi ryan i use stripe checkout and it wooeks fine on desktop but on mobile the data amount is not showing can you help please

Reply

Yo Thierno!

Well, that is weird - we've had some bad luck lately :). How is your checkout form built? Did you use an embedded (non-customized) checkout form from Stripe like we did originally in this tutorial (https://knpuniversity.com/s... or a custom checkout form like we do later https://knpuniversity.com/s... And what exactly doesn't show up (you said "data amount" - where *should* that be printing)?

Let me know - I'm sure we can hunt this down.

Cheers!

Reply
Default user avatar

Thx Ryan the probleme was that my pay button had a strong tag in it so the event was propagated to the strong tag and that tag has no id to get the amount THX

Reply

Ahh, that makes sense. I wonder if e.currentTarget would have gotten the correct element? I think it might have :). Anyways, glad you sorted it out!

Cheers!

Reply
Default user avatar

i didnt even know about the e.currentTarget but it seems to be a good alternatif
Thx

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.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "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", // 1.1.1
        "twig/twig": "^1.24.1", // v1.35.2
        "composer/package-versions-deprecated": "^1.11" // 1.11.99
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.2
        "hautelook/alice-bundle": "^1.3", // v1.3.1
        "doctrine/data-fixtures": "^1.2" // v1.2.1
    }
}
userVoice