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.

Buy Access to Course
35.

Displaying All the Invoice Details

Share this awesome video!

|

Keep on Learning!

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:

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

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

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

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

{% 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:

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

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

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

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

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