If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Ha! We've made it! We've survived our subscription payment setup! So, umm, go celebrate: eat some cake! Sing a song! Or, like we'll do, do some accounting.
Because our last topic is about that: your users will need to see a receipt after purchase. And good news: we've setup our system so that every charge is done by creating an Invoice in Stripe's system. That'll make our life easy.
In fact, all the information we need to render a receipt is already stored in
Stripe. So, you could create a local invoices
database table and store details
there... or, you can take a shortcut and use Stripe's API to fetch invoice data whenever
you actually need it. Let's do that.
Open up StripeClient
. At the bottom, add a new public function findPaidInvoices()
with a User
argument:
... lines 1 - 4 | |
use AppBundle\Entity\User; | |
... lines 6 - 8 | |
class StripeClient | |
{ | |
... lines 11 - 206 | |
public function findPaidInvoices(User $user) | |
{ | |
... lines 209 - 222 | |
} | |
} |
Here's the idea: we'll use Stripe's API to find all Invoices
for a customer,
but then filter those to only return invoices that were paid. That will remove
some garbage invoices the user shouldn't see: like invoices for payments that failed
and then were closed immediately.
Start with: $allInvoices = \Stripe\Invoice::all()
and pass that an array with a
customer
key set to $user->getStripeCustomerId()
:
... lines 1 - 8 | |
class StripeClient | |
{ | |
... lines 11 - 206 | |
public function findPaidInvoices(User $user) | |
{ | |
$allInvoices = \Stripe\Invoice::all([ | |
'customer' => $user->getStripeCustomerId() | |
]); | |
... lines 212 - 222 | |
} | |
} |
Next - and this will look a little weird at first - create an $iterator
variable
set to $allInvoices->autoPagingIterator()
:
... lines 1 - 8 | |
class StripeClient | |
{ | |
... lines 11 - 206 | |
public function findPaidInvoices(User $user) | |
{ | |
$allInvoices = \Stripe\Invoice::all([ | |
'customer' => $user->getStripeCustomerId() | |
]); | |
$iterator = $allInvoices->autoPagingIterator(); | |
... lines 214 - 222 | |
} | |
} |
This is actually really cool: if the user has a lot of invoices, then Stripe will paginate your results. But with the iterator, it will automatically make new API calls behind-the-scenes, allowing us to loop over every invoice, no matter how many there are.
Let's do that: start with $invoices = array()
. Then foreach
over
$iterator as $invoice
:
... lines 1 - 8 | |
class StripeClient | |
{ | |
... lines 11 - 206 | |
public function findPaidInvoices(User $user) | |
{ | |
$allInvoices = \Stripe\Invoice::all([ | |
'customer' => $user->getStripeCustomerId() | |
]); | |
$iterator = $allInvoices->autoPagingIterator(); | |
$invoices = []; | |
foreach ($iterator as $invoice) { | |
... lines 217 - 219 | |
} | |
... lines 221 - 222 | |
} | |
} |
Very simply, we want to know if this invoice is paid. If $invoice->paid
, then
add this to the $invoices
array. Finally, return those paid $invoices
at the bottom:
... lines 1 - 8 | |
class StripeClient | |
{ | |
... lines 11 - 206 | |
public function findPaidInvoices(User $user) | |
{ | |
$allInvoices = \Stripe\Invoice::all([ | |
'customer' => $user->getStripeCustomerId() | |
]); | |
$iterator = $allInvoices->autoPagingIterator(); | |
$invoices = []; | |
foreach ($iterator as $invoice) { | |
if ($invoice->paid) { | |
$invoices[] = $invoice; | |
} | |
} | |
return $invoices; | |
} | |
} |
Heck, let's over-achieve by adding some PHPDoc that shows that this method return
an array of \Stripe\Invoice
objects:
... lines 1 - 8 | |
class StripeClient | |
{ | |
... lines 11 - 202 | |
/** | |
* @param User $user | |
* @return \Stripe\Invoice[] | |
*/ | |
public function findPaidInvoices(User $user) | |
{ | |
... lines 209 - 222 | |
} | |
} |
Thanks to this function, on the account page, at the bottom, we'll print a list of all the Customer's invoices. Eventually, they'll be able to click each invoice to see all the details.
Start in ProfileController
... all the way at the top: this method renders the account
page. Fetch the invoices with
$invoices = $this->get('stripe_client')->findPaidInvoices()
with the current user.
Pass that as a new variable into the template:
... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
... lines 17 - 19 | |
public function accountAction() | |
{ | |
... lines 22 - 35 | |
$invoices = $this->get('stripe_client') | |
->findPaidInvoices($this->getUser()); | |
return $this->render('profile/account.html.twig', [ | |
... lines 40 - 44 | |
'invoices' => $invoices | |
]); | |
} | |
... lines 48 - 177 | |
} |
Now inside the template, find the bottom of the table. Add a new row, and title it Invoices:
... 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> | |
... lines 152 - 160 | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
... lines 166 - 174 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 179 - 180 |
Next, create a list and then loop with for invoice in invoices
. Add the endfor
.
Create an anchor tag, but keep the href
empty for now - we don't have an invoice
"show" page yet. Add some classes to make this look a little fancy:
... 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="" class="list-group-item"> | |
... lines 155 - 157 | |
</a> | |
{% endfor %} | |
</div> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
... lines 166 - 174 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 179 - 180 |
So let's see, the user might be looking for a specific invoice, so let's print
its date. Check out the Invoice API. Hey! There's actually a date
field, which
is a UNIX timestamp. Print invoice.date
and then pipe that through the date
filter
with Y-m-d
:
... 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="" class="list-group-item"> | |
Date: {{ invoice.date|date('Y-m-d') }} | |
... lines 156 - 157 | |
</a> | |
{% endfor %} | |
</div> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
... lines 166 - 174 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 179 - 180 |
Next, add a span label, float it right, and inside, add the amount: $
then print
invoice.amount_due / 100
to convert it from cents to dollars:
... 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="" 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 |
The amount_due
field is what the user should have actually been charged,
after accounting for coupons or a positive account balance.
Try things out so far: head back to the account page and refresh! Bam! Here's our long invoice list. Next, let's give each invoice its own detailed display page, complete with invoice items, discounts and anything else that might have happened on that invoice.
"Houston: no signs of life"
Start the conversation!
// 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
}
}