next(self::CIRCLE_STRIPE); } public function nextManualNumber(): string { return $this->next(self::CIRCLE_MANUAL); } public function next(string $circle): string { if (! in_array($circle, [self::CIRCLE_STRIPE, self::CIRCLE_MANUAL], true)) { throw new InvalidArgumentException("Unbekannter Rechnungskreis: {$circle}"); } $number = DB::transaction(function () use ($circle): int { // Sequenz-Zeile race-sicher sicherstellen: insertOrIgnore nutzt den // Unique-Index auf `circle`, sodass parallele Erstaufrufe nicht in // doppelte Zeilen (und damit doppelte Nummern) laufen. DB::table('invoice_number_sequences')->insertOrIgnore([ 'circle' => $circle, 'next_number' => 1, 'created_at' => now(), 'updated_at' => now(), ]); $sequence = DB::table('invoice_number_sequences') ->where('circle', $circle) ->lockForUpdate() ->first(); DB::table('invoice_number_sequences') ->where('id', $sequence->id) ->update(['next_number' => $sequence->next_number + 1, 'updated_at' => now()]); return (int) $sequence->next_number; }); $padding = (int) config('billing.invoice_number_padding', 5); return sprintf('%s-%s', $circle, str_pad((string) $number, $padding, '0', STR_PAD_LEFT)); } }