Back to Course |
Structuring Databases in Laravel 11

Invoice Numbers with Series Prefixes

In this lesson, let's examine the database structure for invoice numbers. For example, an invoice number can start with a series or prefix and then a number leading with zeros, like INT-001.

Another example could be a year and then 001 after the dash, like 2024-001. Or some word and then the number, like Mars-108.

So, how do you store that in the database and make that incremental?


Eloquent Mutators

Imagine a simplified Invoice table where we save the invoice number in the invoice_number column.

Schema::create('invoices', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id');
$table->unsignedBigInteger('invoice_number');
$table->integer('paid_total');
$table->timestamps();
});

How do you set the invoice_number column to be incremented when the invoice is created? Probably the easiest way would be to use an Eloquent mutator.

app/Models/Invoice.php:

use Illuminate\Database\Eloquent\Casts\Attribute;
 
class Invoice extends Model
{
protected $fillable = [
'user_id',
'invoice_number',
'paid_total',
];
 
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
 
protected function invoiceNumber(): Attribute
{
return Attribute::make(
set: fn () => Invoice::max('invoice_number') + 1,
);
}
}

The downside of this method is you still need to pass some value to the invoice_number when creating a record.

App\Models\Invoice::create(['user_id' => 1, 'paid_total' => 6969, 'invoice_number' => 0]);

And it creates the record.

App\Models\Invoice {#6048
user_id: 1,
paid_total: 6969,
invoice_number: 2,
updated_at: "2024-06-25 10:57:26",
created_at: "2024-06-25 10:57:26",
id: 2,
}

Now, how do you get the full invoice number? One way could be to use an Eloquent accessor.

app/Models/Invoice.php:

class Invoice extends Model
{
protected $fillable = [
'user_id',
'invoice_number',
'paid_total',
];
 
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
 
protected function invoiceNumber(): Attribute
{
return Attribute::make(
get: fn(int $value) => 'ABC-' . $value,
set: fn () => Invoice::max('invoice_number') + 1,
);
}
}

The result for the latest invoice would be similar to this:

> $invoice = App\Models\Invoice::latest()->first();
= App\Models\Invoice {#5742
id: 2,
user_id: 1,
invoice_number: 2,
paid_total: 6969,
created_at: "2024-06-25 10:57:26",
updated_at: "2024-06-25 10:57:26",
}
 
> $invoice->invoice_number;
= "ABC-2"

But what about leading zeros? To add zeros, we can use a PHP function str_pad().

app/Models/Invoice.php:

class Invoice extends Model
{
protected $fillable = [
'user_id',
'invoice_number',
'paid_total',
];
 
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
 
protected function invoiceNumber(): Attribute
{
return Attribute::make(
get: fn(int $value) => 'ABC-' . $value,
get: fn(int $value) => 'ABC-' . str_pad($value, 5, '0', STR_PAD_LEFT),
set: fn () => Invoice::max('invoice_number') + 1,
);
}
}

Now, for the same invoice, the result would look like this:

> $invoice = App\Models\Invoice::latest()->first();
= App\Models\Invoice {#5742
id: 2,
user_id: 1,
invoice_number: 2,
paid_total: 6969,
created_at: "2024-06-25 10:57:26",
updated_at: "2024-06-25 10:57:26",
}
 
> $invoice->invoice_number;
= "ABC-00002"

Invoice Series for Users?

What if the invoice must be set based on the user? Suppose we have an invoice_series column in the users table, which should be used for the prefix. If the prefix isn't set, the fallback should be the year.

app/Models/Invoice.php:

class Invoice extends Model
{
protected $fillable = [
'user_id',
'invoice_number',
'paid_total',
];
 
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
 
protected function invoiceNumber(): Attribute
{
return Attribute::make(
get: fn(int $value) => 'ABC-' . str_pad($value, 5, '0', STR_PAD_LEFT),
set: fn () => Invoice::max('invoice_number') + 1,
get: function(int $value) {
$series = $this->user->invoice_series ?? date('Y');
 
return $series . '-' . str_pad($value, 5, '0', STR_PAD_LEFT);
},
set: fn (mixed $value, array $attributes) => Invoice::where('user_id', $attributes['user_id'])->max('invoice_number') + 1,
);
}
}

When the invoice_series for the user isn't set, then the prefix is set to the year.

> $invoice = App\Models\Invoice::latest()->first();
= App\Models\Invoice {#5041
id: 3,
user_id: 1,
invoice_number: 3,
paid_total: 6969,
created_at: "2024-06-25 11:19:24",
updated_at: "2024-06-25 11:19:24",
}
 
> $invoice->invoice_number;
= "2024-00003"

But when the invoice_series for the user is set, it is used as a prefix.

> $invoice = App\Models\Invoice::latest()->first();
= App\Models\Invoice {#5742
id: 3,
user_id: 1,
invoice_number: 3,
paid_total: 6969,
created_at: "2024-06-25 11:19:24",
updated_at: "2024-06-25 11:19:24",
}
 
> $invoice->invoice_number;
= "XYZ-00003"

These are short tips on how to deal with invoice numbers in the database and Eloquent within Laravel.

You can also read this article: Laravel Invoices: Auto-Generate Serial Numbers - 4 Different Ways.