From f00ba326434a24966bb885225d111bea931b62a0 Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Wed, 11 Mar 2026 19:24:06 -0400 Subject: [PATCH] Fix invoice number race condition: use DB lock for atomic increment --- app/Listeners/InvoiceCreatingListener.php | 32 +++++++++++++++++------ 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/app/Listeners/InvoiceCreatingListener.php b/app/Listeners/InvoiceCreatingListener.php index 276fc01b1..48a6505b2 100644 --- a/app/Listeners/InvoiceCreatingListener.php +++ b/app/Listeners/InvoiceCreatingListener.php @@ -5,6 +5,7 @@ namespace App\Listeners; use App\Events\Invoice\Creating; use App\Models\Invoice; use App\Models\Setting; +use Illuminate\Support\Facades\DB; class InvoiceCreatingListener { @@ -13,15 +14,30 @@ class InvoiceCreatingListener */ public function handle(Creating $event): void { + // Use database lock to prevent duplicate invoice numbers across clustered nodes + $number = DB::transaction(function () { + $setting = Setting::where('key', 'invoice_number') + ->whereNull('settingable_id') + ->lockForUpdate() + ->first(); + + $current = $setting ? (int) $setting->value : (int) config('settings.invoice_number', 0); + $next = $current + 1; + + Setting::updateOrCreate([ + 'key' => 'invoice_number', + 'settingable_id' => null, + 'settingable_type' => null, + ], [ + 'value' => $next, + ]); + + // Update in-memory config so subsequent calls in the same request are correct + config(['settings.invoice_number' => $next]); + + return $next; + }); - // Get the next invoice number - $number = config('settings.invoice_number', 1) + 1; - // Update setting - Setting::updateOrCreate([ - 'key' => 'invoice_number', - ], [ - 'value' => $number, - ]); // Pad the invoice number with leading zeros $paddedNumber = str_pad($number, config('settings.invoice_number_padding', 1), '0', STR_PAD_LEFT);