DEV Community

Daveyon Mayne 😻
Daveyon Mayne 😻

Posted on • Edited on

Implementing an Invoice Numbering System with PHP using Laravel 6

In my last post in How I believe Xero pulled off implementing their Invoice Numbering System, I showed you how I created a customisable and incremental invoice numbering system using Ruby on Rails. This post will demonstrate how it's done using PHP (Laravel 6).

To recap, we have two sections that make up an invoice number:

  1. The Prefix
  2. A numeric value

This approach is customisable as a user can change the prefix to match their business needs and start the sequential value at any number ie 001, 0001 or 1,
max ten characters. 001 is widely used.

In your terminal, run:

php artisan make:migration create_invoice_settings_table
Enter fullscreen mode Exit fullscreen mode

Now, edit the migration file to match below:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateInvoiceSettingsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('invoice_settings', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('organisation_id')->unsigned();
            $table->string('prefix')->default('INV-');
            $table->string('number_sequence')->default('001');

            // You may have other fields...

            // The association
            $table->foreign('organisation_id')->references('id')->on('organisations');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('invoice_settings');
    }
}
Enter fullscreen mode Exit fullscreen mode

Run your migration to create the table with default values.

A new InvoiceSettings should be created when a organisation is added to your system, that way we have the default data assigned to an organisation or create a Seeder to create this recored for the current organisation.

Our InvoiceSettings controller would look as follows:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
// use App\InvoiceSettings;

class InvoiceSettingsController extends Controller
{
    // A constructor will not be used in this example.

    /**
     * Gets the defaults values.
     *
     * @param  int  $organisationId
     * @return Response
     */
    public function show($organisationId)
    {
        //
    } 

    /**
     * Update the invoice settings for the given organisation.
     *
     * @param  request  $request
     * @return Response
     */
    public function update(Request $request)
    {
        //
    }
}
Enter fullscreen mode Exit fullscreen mode

Go ahead and create an invoice settings model with migration etc then uncomment InvoiceSettings alias in the above when finished.

Validation

Most Laravel developer place their validations in the controller, we won't be doing that, you should not be doing that. Our controllers should be easily testable, slim and saves item to the database/retrieves items from database etc.

If you have created a route for localhost:4000/organisations/1/invoice-settings, you should see a form with two fields, populating the default invoice settings values. Go ahead and implement that yourself by using an api route or using the blade template.

Once you've done that, we'll create a Laravel Form Request

# Spell this how you see fit
php artisan make:request ShouldBeSequentializeAndUnique
Enter fullscreen mode Exit fullscreen mode
[...]
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;

[...]

/**
 * Get the custom validation rules that apply to the request.
 *
 * @return array
 */
public function rules()
{
    return $this->customRule();
}

/**
 * Validate sequence number is numeric and not already in database.
 * 
 * @return array|exception
 */
private function customRule()
{
    $prefix = $this->prefix;
    $numberSequence = $this->number_sequence;

    // In most cases, you should have this already in your controller,
    // but we need it here also.
    $organisationId = request()->organisation_id;
    $invoiceReferenceNumber = strtolower($prefix . $numberSequence); // we now/should have something like inv-001

    $rules = [
      'prefix'          => 'required|string|max:10',
      'organisation_id' => 'required|integer',

         'number_sequence' => [
           'required', 'numeric', 'digits_between:1,10',

            Rule::unique('some-table')->where(function ($query) use ($organisationId, $invoiceReferenceNumber) {
              $q = $query
                  ->whereRaw('LOWER(number) LIKE ?', '%' . $invoiceReferenceNumber . '%')
                  ->where('organisation_id', $organisationId)->first();
              if ($q) {
                  throw ValidationException::withMessages(['number_sequence' => 'Sequence number already in use']);
              }
              return $q;
            })
          ],
    ];

    return $rules;
}
Enter fullscreen mode Exit fullscreen mode

WOW! What's going here? For starters, prefix should be a string with a max length of 10 characters. number_sequence should be numeric with a max length of 10 characters. Noticed we have not used max:10 as the input field type should be number. When it's set to type=number, max:10 sums up the value; instead, we use digits_between 1 and 10.

What is some-table? For your homework, create another migration called invoices with a column called number of type string (required), then replace some-table with invoices. Obviously you'll need other columns etc.

Our Rule searches for invoices with the same $invoiceReferenceNumber and if found then $numberSequence is already in use as we combined prefix + numberSequence to make an invoice reference number.

Generating an invoice with number iNv-001 will throw an error*. Generating an invoice with ii-001 will not throw an error* as the user only wants to change the prefix.

This error* only refers to updating the invoice settings, not when creating an invoice. For your homework, create a validation to ensure number is unique for all invoices but only per user's organisation.

Our controller should now look like:


<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Http\Requests\ShouldBeSequentializeAndUnique;
use App\InvoiceSettings;

class InvoiceSettingsController extends Controller
{
    // A constructor will not be used in this example.


    /**
     * Update the invoice settings for the given organisation.
     *
     * @param  ShouldBeSequentializeAndUnique  $request
     * @return Json
     */
    public function update(ShouldBeSequentializeAndUnique $request)
    {
        // Run the validation.
        $validated = $request->validated();

        // $organisationId comes from session or the param.

       // Everything should be ok from here.
       InvoiceSettings::where('organisation_id', $organisationId)
           ->update([
                 'prefix'          => $request->prefix,
                 'number_sequence' => $request->number_sequence
             ]);

      return response()->json($validated, 200);
    }
}

Enter fullscreen mode Exit fullscreen mode

EDIT per comment

public function update(ShouldBeSequentializeAndUnique $request)
    {     
       // $organisationId comes from session or the param.

       // Everything should be ok from here.
       $settings = InvoiceSettings::updateOrCreate(
         ['organisation_id'   => $request->organisation_id],
         [
            'prefix'          => $request->prefix,
            'number_sequence' => $request->number_sequence
         ]);

      return response()->json($settings, 200);
    }
Enter fullscreen mode Exit fullscreen mode

There are many ways write code. The above maybe incorrect for some but, as we all say, "it works for me" 😁. I'm open for improvements as I haven't been professionally using Laravel for years.

In another post, I'll demonstrate how to increment the number_sequence on every invoice creation.

Top comments (3)

Collapse
 
stetim94 profile image
stetim94

FormRequest is a child class of Request, so doing this:

    $prefix = request()->prefix;
    $numberSequence = request()->number_sequence;
Enter fullscreen mode Exit fullscreen mode

feels like an anti-pattern, you rely on the container to give you the current request, while in a child class of request. you can simply do:

    $prefix = $this->prefix;
    $numberSequence = $this->number_sequence;
Enter fullscreen mode Exit fullscreen mode

or even use any method available in request class:

    $prefix = $this->input('prefix');
    $numberSequence = $this->input('number_sequence');
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mirmayne profile image
Daveyon Mayne 😻

Awesome! Thanks for that!

Collapse
 
mirmayne profile image
Daveyon Mayne 😻

Thank you! I shall use and update.