Buy

Displaying All the Invoice Details

To see all the details for each invoice, we need an "invoice show" page. Head to ProfileController and add a new method for this: showInvoiceAction(). Give it a URL: /profile/invoices/{invoiceId}. And a name: account_invoice_show, and then add the $invoiceId argument:

192 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 14
class ProfileController extends BaseController
{
... lines 17 - 178
/**
* @Route("/profile/invoices/{invoiceId}", name="account_invoice_show")
*/
public function showInvoiceAction($invoiceId)
{
... lines 184 - 189
}
}

Before doing anything else, copy that route name, head back to the account template and fill in the href by printing path() then pasting the route name. For the second argument, pass in the wildcard: invoiceId set to invoice.id, which will be the Stripe invoice ID:

180 lines app/Resources/views/profile/account.html.twig
... lines 1 - 67
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
... lines 73 - 88
<table class="table">
<tbody>
... lines 91 - 148
<tr>
<th>Invoices</th>
<td>
<div class="list-group">
{% for invoice in invoices %}
<a href="{{ path('account_invoice_show', {invoiceId: invoice.id}) }}" class="list-group-item">
Date: {{ invoice.date|date('Y-m-d') }}
<span class="label label-success pull-right">${{ invoice.amount_due/100 }} </span>
</a>
{% endfor %}
</div>
</td>
</tr>
</tbody>
</table>
</div>
... lines 166 - 174
</div>
</div>
</div>
{% endblock %}
... lines 179 - 180

Fetch One Invoice's Data

Back in the controller, our work here is pretty simple: we'll just ask Stripe for this one Invoice. In StripeClient, we don't have a method that returns just one invoice, so let's add one: public function findInvoice() with an $invoiceId argument. Inside, return the elegant \Stripe\Invoice::retrieve($invoiceId):

230 lines src/AppBundle/StripeClient.php
... lines 1 - 8
class StripeClient
{
... lines 11 - 224
public function findInvoice($invoiceId)
{
return \Stripe\Invoice::retrieve($invoiceId);
}
}

Love it!

In the controller, use this: $stripeInvoice = $this->get('stripe_client')->findInvoice() with $invoiceId:

192 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 14
class ProfileController extends BaseController
{
... lines 17 - 181
public function showInvoiceAction($invoiceId)
{
$stripeInvoice = $this->get('stripe_client')
->findInvoice($invoiceId);
... lines 186 - 189
}
}

To make things really nice, you'll probably want to wrap this in a try-catch block: if there's a 404 error from Stripe, you'll want to catch that exception and throw the normal $this->createNotFoundException(). That'll cause our site to return a 404 error, instead of 500 error.

Finally, render a new template: how about profile/invoice.html.twig. Pass this an invoice variable set to $stripeInvoice:

192 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 14
class ProfileController extends BaseController
{
... lines 17 - 181
public function showInvoiceAction($invoiceId)
{
$stripeInvoice = $this->get('stripe_client')
->findInvoice($invoiceId);
return $this->render('profile/invoice.html.twig', array(
'invoice' => $stripeInvoice
));
}
}

Rendering Invoice Details

Instead of creating that template by hand, let's take a shortcut. If you downloaded the "start" code from the site, you should have a tutorial/ directory with an invoice.html.twig file inside. Copy that and paste it into your profile/ templates directory:

79 lines app/Resources/views/profile/invoice.html.twig
{% extends 'base.html.twig' %}
{% import _self as macros %}
{% macro currency(rawStripeAmount) %}
{% if rawStripeAmount < 0 %}-{% endif %}${{ (rawStripeAmount/100)|abs }}
{% endmacro %}
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
<h1>Invoice {{ invoice.date|date('Y-m-d') }}</h1>
<table class="table">
<thead>
<tr>
<th>To</th>
{# or put company information here #}
<th>{{ app.user.email }}</th>
</tr>
<tr>
<th>Invoice Number</th>
<th>
{{ invoice.id }}
</th>
</tr>
</thead>
</table>
<table class="table table-striped">
<tbody>
{% if invoice.starting_balance %}
<tr>
<td>Starting Balance</td>
<td>
{{ macros.currency(invoice.starting_balance) }}
</td>
</tr>
{% endif %}
{% for lineItem in invoice.lines.data %}
<tr>
<td>
{% if lineItem.description %}
{{ lineItem.description }}
{% elseif (lineItem.plan) %}
Subscription to {{ lineItem.plan.name }}
{% endif %}
</td>
<td>
{{ macros.currency(lineItem.amount) }}
</td>
</tr>
{% endfor %}
{% if invoice.discount %}
<tr>
<td>Discount: {{ invoice.discount.coupon.id }}</td>
<td>
{{ macros.currency(invoice.discount.coupon.amount_off * -1) }}
</td>
</tr>
{% endif %}
<tr>
<th>Total</th>
<th>
{{ macros.currency(invoice.amount_due) }}
</th>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

Before we look deeper at this, let's make sure it works! Refresh the profile page, then click into one of the discounted invoices. Score! It's got all the important stuff: the subscription, the discount and the total at the bottom.

Here's the deal: there is an infinite number of ways to render an invoice. But the tricky part is understanding all the different pieces that you need to include. Let's take a look at invoice.html.twig to see what it's doing.

The Components of an Invoice

First, the Invoice has a starting_balance field:

79 lines app/Resources/views/profile/invoice.html.twig
... lines 1 - 7
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
... lines 14 - 31
<table class="table table-striped">
<tbody>
{% if invoice.starting_balance %}
<tr>
<td>Starting Balance</td>
<td>
{{ macros.currency(invoice.starting_balance) }}
</td>
</tr>
{% endif %}
... lines 42 - 71
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

This answers the question: "how much was the customer's account balance before charging this invoice?" If the balance is positive, then this was used to discount the invoice before charging the customer. By printing it here, it'll help explain the total.

Tip

It's possible that not all of the Customer's balance was used. You could also use the ending_balance field to check.

Second, since each charge is a line item, we can loop through each one and print its details. But, each line item might be for an individual product or for a subscription. It's a little weird, but I've found that the best way to handle this is to check to see if lineItem.description is set:

79 lines app/Resources/views/profile/invoice.html.twig
... lines 1 - 7
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
... lines 14 - 31
<table class="table table-striped">
<tbody>
... lines 34 - 41
{% for lineItem in invoice.lines.data %}
<tr>
<td>
{% if lineItem.description %}
{{ lineItem.description }}
{% elseif (lineItem.plan) %}
Subscription to {{ lineItem.plan.name }}
{% endif %}
</td>
<td>
{{ macros.currency(lineItem.amount) }}
</td>
</tr>
{% endfor %}
... lines 56 - 71
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

If it is set, then print it. In that case, this line item is either an individual product - in which case the description is the product's name - or it's a proration subscription line item that's created when a user changes between plans. In that case, the description is really nice: it explains exactly what this charge or credit means.

But if the description is blank, this is for a normal subscription charge. Print out "Subscription to" and then lineItem.plan.name.

In both cases, for the amount, print lineItem.amount. Oh, that macros.currency() thing is a macro I setup that helps manage negative numbers and adds the $ sign:

79 lines app/Resources/views/profile/invoice.html.twig
... line 1
{% import _self as macros %}
{% macro currency(rawStripeAmount) %}
{% if rawStripeAmount < 0 %}-{% endif %}${{ (rawStripeAmount/100)|abs }}
{% endmacro %}
... lines 7 - 79

After the line items, there's just one more thing to worry about: discounts! We already know that you can create Coupons and attach them to a Customer at checkout. When a Coupon has been used, it's known as a "discount" on the invoice. Let's print the coupon's ID and the amount off thanks to the coupon:

79 lines app/Resources/views/profile/invoice.html.twig
... lines 1 - 7
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
... lines 14 - 31
<table class="table table-striped">
<tbody>
... lines 34 - 56
{% if invoice.discount %}
<tr>
<td>Discount: {{ invoice.discount.coupon.id }}</td>
<td>
{{ macros.currency(invoice.discount.coupon.amount_off * -1) }}
</td>
</tr>
{% endif %}
... lines 65 - 71
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

If you want to support Coupons for both a set amount off and a percentage off, you'll need to do a little bit more work here.

Finally, at the bottom: print the total by using the amount_due field. After taking everything above into account, this should be the amount they were charged:

79 lines app/Resources/views/profile/invoice.html.twig
... lines 1 - 7
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
... lines 14 - 31
<table class="table table-striped">
<tbody>
... lines 34 - 65
<tr>
<th>Total</th>
<th>
{{ macros.currency(invoice.amount_due) }}
</th>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

So... Store Invoices Locally?

Ok! Once you know which fields to render, it's not too bad. But this approach has one big downside: we don't have any of the invoice data in our database: we're relying on a third party to store everything. It also means that it'll be a little bit harder to query or report on invoice data. And finally, the invoice pages may load a little slow, since we're waiting for an API request to Stripe to finish.

If you do want to store invoices locally, it's not too much more work. Of course, you'll need an invoices table with whatever columns are important for you to store: like the amount charged, and maybe some discount details.

That's simple enough, but how and when would we populate this? Webhooks! Specifically, the invoice.created webhook: just respond to this and create a "copy" in your database of whatever info you want. You'll also want to listen to invoice.updated to catch any changes to an invoice, like when it goes from unpaid to paid.

If that's important to you, go for it!

Ahhhh, now we really did it! We've made it to the end. This stuff is tough, but you should feel empowered. Creating a payment system is about more than just accepting credit cards, it's about giving your customers a great, bug-free, surprise-free and joyful experience. Go out there and make that a reality!

And as always, if you have any questions, ask us in the comments.

Seeya guys next time!

Leave a comment!

  • 2017-03-13 weaverryan

    Hey Blueblazer172!

    First, congrats! This were 2 BIG tutorials... I remember well when I recorded them :).

    1) There's nothing specific to Stripe that you'll need to change in production, besides changing your Stripe API key to be your production key, and making sure you're forcing https (which of course you are - since we've been talking about that elsewhere!). If there's anything else, it's specific to Symfony, and it's related to performance. We've got some details about that here: http://symfony.com/doc/curr... - but it's nothing mission critical.

    2) Symfony itself is secure (we handle security patches, etc), but of course it doesn't mean that code we write in Symfony is secure :). Thanks to Doctrine, you're already safe from SQL injection attacks (unless you're working around Doctrine in some weird way). And in the tutorial, we've been very sensitive to *never* submit the credit card information to our server. In Symfony, the easiest way to mess up security is... well... to forget to secure sensitive pages. For example, if you have a /admin section... but you forget to check to make sure the user is an admin, then there's your security hole :). Make sure each endpoint is properly secured - that will do a lot. Here in KnpU, we also make sure not to save any personal data we don't need to - e.g. we don't store a user's address anywhere. Obviously, the less data you can store in your server, the better.

    3) Yep, the biggest things is to change the API key to live mode (though you'll do this actually in parameters.yml - http://knpuniversity.com/sc.... You'll of course need to make sure your "live mode" is activated on Stripe - iirc they have a few setup things to do that. And finally, don't forget that we setup our Plans in Stripe manually. So the plans will need to be manually setup on production again, with the same plan id's (so that they match our code). You don't need to delete your test data - it's sort of unrelated to changing to "prod" mode.

    Oh, one last thing! You should also look into PCI compliance - Stripe makes it easy, but there's some work you might want to look into doing.

    Good luck! It's been cool to see your progress!

  • 2017-03-10 Blueblazer172

    Wow I've made it through all 2 Parts :)

    Now my last questions will be:

    1. What should I do/change to run all in production?
    2. What is important to know about the right security in symfony?
    3. How should I setup Stripe? Should I Only delete all testdata and change to live mode and change the api keys in the config.yml? or is there something else i should know when switching to live?

    You guys are amazing. It is worth spending my time on your tuts, cause they are easy to understand and easy to follow :)
    Keep up that great work.