Our custom setup for sending out invoices

Screw you and your fancy codebase

Published in PHP on Apr 28, 2022

A client I work for is organizing a conference. This conference primarily targets business customers, and my client wanted a solution for sending invoices based on a form on the website. Existing solutions did not meet the requirements, so they needed a custom solution.

This is where Moneybird comes in

We decided to work with Moneybird. That’s a Dutch accounting software provider. They are hugely popular in the Netherlands and Belgium and have an excellent API.

The advantage of working with Moneybird is that we didn’t have to design invoices, nor did we have to deal with flows such as those of sending reminders or processing payments. Moneybird handles that for us.

All we have to do is:

  1. Creating a new contact
  2. Creating a new invoice
  3. Sending the invoice

Everything else is handled by Moneybird.

Using picqer/moneybird-php-client

Even though Moneybird has an excellent REST API, we decided to use an existing open source package to make it easier to work with: picqer/moneybird-php-client.

Because of this, we didn’t need to set up requests or think about handling errors. The package allows you to create new objects, and saving them will automatically send them to Moneybird.

To understand what this means, let’s take a look at some example code from the projects README:

// Example: Create a new contact
$contact = $moneybird->contact();

$contact->company_name = 'Picqer';
$contact->firstname = 'Stephan';
$contact->lastname = 'Groen';
$contact->save();
var_dump($contact); // Contact object (as saved in Moneybird)

This technique is super easy to use, and the code that powers it, is very simple. If you are not interested, you can skip this code completely.

// File: moneybird-php-client/src/Picqer/Financials/Moneybird/Actions/Storable.php 

 <?php

namespace Picqer\Financials\Moneybird\Actions;

/**
 * Class Storable.
 */
trait Storable
{
    use BaseTrait;

    public function save()
    {
        if ($this->exists()) {
            return $this->update();
        } else {
            return $this->insert();
        }
    }
    
    public function insert()
    {
        $result = $this->connection()->post($this->getEndpoint(), $this->jsonWithNamespace());

        if (method_exists($this, 'clearDirty')) {
            $this->clearDirty();
        }

        return $this->selfFromResponse($result);
    }
    
    public function update()
    {
        $result = $this->connection()->patch($this->getEndpoint() . '/' . urlencode($this->id), $this->jsonWithNamespace());

        if ($result === 200) {
            if (method_exists($this, 'clearDirty')) {
                $this->clearDirty();
            }

            return true;
        }

        return $this->selfFromResponse($result);
    }
}

I personally love this technique, because it makes it very easy to work with the API.

Our implementation

Before we can start implementing our three steps, we need to configure our code to use the Moneybird API.

<?php

// Connect to Moneybird
$connection = new Connection();
$connection->setAccessToken('YOUR_ACCESS_TOKEN');
try {
    $connection->connect();
} catch (\Exception $e) {
    throw new Exception('Could not connect to Moneybird: ' . $e->getMessage());
}

// Set up Moneybird client
$moneybird = new Moneybird($connection);

// Configure default administration
$administrations = $moneybird->administration()->getAll();
$connection->setAdministrationId($administrations[0]->id);

Now that we have set up our connection, we can start creating our contact and invoice. We do this procedurally. We could of course create a class to manage this process, write functions for all the steps and bla bla bla. But why would we?

This endpoint isn’t meant to be reused. It takes information from the form as input, sets the right values and sends it to Moneybird. That’s it.

Of course, I can’t share the complete file with you. What I can do however, is tell you what it looks like.

<?php

use Picqer\Financials\Moneybird\Connection;
use Picqer\Financials\Moneybird\Moneybird;

require __DIR__ . '/vendor/autoload.php';

// Get parameters from the request
$company_name = $_POST['company_name'] ?? '';
// ...

// Connect to Moneybird
$connection = new Connection();
$connection->setAccessToken('YOUR_ACCESS_TOKEN');
// ...

// Set up Moneybird client
$moneybird = new Moneybird($connection);

// Configure default administration
$administrations = $moneybird->administration()->getAll();
$connection->setAdministrationId($administrations[0]->id);

// Create contact
$contact = $moneybird->contact();
$contact->company_name = $company_name;
// ...

// Create contact
$contact->save();
$contact_id = $contact->id;
// ...

// Create invoice
$invoice = $moneybird->salesInvoice();
$invoice->contact_id = $contact_id;
// ..

// Set invoice details
$invoice_detail = $moneybird->salesInvoiceDetail();
$invoice_detail->amount = '1';
// ...

// Early bird discount
if (date('Y-m-d') <= '2022-05-15') {
    // Set discount ...
}

// Create invoice
$invoice->save();

// Send invoice
$invoice->sendInvoice();

I left out some parts about references and comments for example, but the structure is very clear. The script doesn’t have a single function. There’s not a single standard we are correctly following.

And you know what? I couldn’t care less. The complete setup was finished within 2 hours, and it works perfectly. My client is happy with the result and so am I.

We deployed the script to an existing EC2 instance and configured the form on the website to send all requests to this script. The end result? Invoices are sent immediately, with the option to automatically register payments, send reminders, modify the invoice design, design custom workflows and much more.

I know this isn’t what you like to hear, but a lot of your code could probably also be a single, ugly PHP file and no one except for you would ever know.

Unless you were to write a blog post about it, of course.

Oh, one more thing

The package we are using, didn’t have a field in one of its models, which it should have according to the API. So instead of flaming the developer on Reddit, I added a field to the model and submitted a pull request.

The result? The package has now been updated and the field is there.

It’s that simple.