Implementing an Invoice Numbering System with PHP using Laravel 8

Build an invoice numbering system using PHP with Laravel.

Implementing an Invoice Numbering System with PHP using Laravel 8

I always wondered how invoice companies build their invoice numbering system ie INV-001 etc. I don't know but I assume it goes like this.

Should I break down the invoice number INV-001, it has two parts:
1. The Prefix: INV-
2. A numeric value: 001

This approach is customisable as a user should be able change the prefix to match their business needs and start the sequential value at any numeric value ie 001, 0001 or 1.

In your terminal, run

php artisan make:model InvoiceSetting -m

This command creates a model file and a migration file. We're going to focus only on the migration file:

<?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');
    }
}

I'm using an organisation_id as a reference as in my code; an organisation will have many invoices. For you, you may have a customer or some other association.

Model names are always singular. "Invoice Setting" may not feels natural but "Invoice Settings" may. If you do make the model name plural, which I advise not to, InvoiceSettings.php, you will need to specify the table it's referring to:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class InvoiceSettings extends Model
{
    use HasFactory;
    
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'invoice_settings';
    
}

If you've kept your model name as singular, the protected $table is not needed. Next, the controller:

<?php

namespace App\Http\Controllers;

use App\Models\InvoiceSetting;

class InvoiceSettingsController extends Controller
{
    public function __construct(){}

    /**
     * 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)
    {
        //
    }
}

Validation

Validations should not live in the controller, we won't be doing that, you should not be doing that. Our controllers should be easily testable, slim that performs business logics. Now create a Form Request:

php artisan make:request ShouldBeSequentializeAndUnique
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;
}

So what's going on 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 an invoice model/migration  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 now becomes:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Http\Requests\ShouldBeSequentializeAndUnique;
use App\Models\InvoiceSetting;

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)
    {
       // Everything should be ok from here.
       $settings = InvoiceSetting::updateOrCreate(
         ['organisation_id'   => $request->organisation_id],
         [
            'prefix'          => $request->prefix,
            'number_sequence' => $request->number_sequence
         ]);

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

You now have the back-end setup, now create the front-end then make a POST request with an input field. Ensure to create a model for organisation where an organisation can have multiple invoices. Also ensure that your organisations cannot have duplicate invoice numbers. You're looking for a unique column on the organisation_id and number. Have fun figuring out what model(s) this should be on. After all, that's how we learn. ✌🏼