id(); $table->uuid()->unique(); // 'invoice' | 'credit_note' | 'cancellation' | 'deposit'. A string // rather than an enum column: adding a kind must not be a migration // on a table that holds legal records. $table->string('kind', 32); $table->string('name'); // "RE", "GS", "ST" — what the number starts with. $table->string('prefix', 12); // The year is part of the number and resets the counter, or it is // not and does not. Both are lawful; mixing them is not, so it is // recorded per series rather than assumed. $table->boolean('yearly_reset')->default(true); $table->unsignedSmallInteger('digits')->default(4); // The counter itself. Never derived from the invoices table: a // deleted or failed row would silently reuse a number that has // already been on a document somebody has. $table->unsignedInteger('next_number')->default(1); $table->unsignedSmallInteger('counter_year')->nullable(); $table->boolean('active')->default(true); $table->timestamps(); $table->unique(['kind', 'prefix']); }); Schema::create('invoices', function (Blueprint $table) { $table->id(); $table->uuid()->unique(); $table->foreignId('invoice_series_id')->constrained()->restrictOnDelete(); $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete(); $table->foreignId('order_id')->nullable()->constrained()->nullOnDelete(); // The number as it appears on the document, and its parts, so a // list can be sorted and filtered without parsing the string back. $table->string('number')->unique(); $table->unsignedSmallInteger('number_year')->nullable(); $table->unsignedInteger('number_sequence'); $table->date('issued_on'); $table->date('due_on')->nullable(); // Everything the document says. See the class comment: this is what // makes the PDF reproducible without storing the PDF. $table->json('snapshot'); // Denormalised out of the snapshot, because a list of invoices has // to be sortable by money without opening every JSON column. $table->integer('net_cents'); $table->integer('tax_cents'); $table->integer('gross_cents'); $table->string('currency', 3); // A cancellation points at what it cancels. Nothing is ever // deleted or edited — a wrong invoice is corrected by issuing // another document, which is the only lawful way to correct one. $table->foreignId('cancels_invoice_id')->nullable()->constrained('invoices')->nullOnDelete(); $table->timestamp('sent_at')->nullable(); $table->timestamps(); $table->index(['customer_id', 'issued_on']); }); // The four an Austrian business needs on day one. Seeded rather than // left to the operator: an installation with no series cannot issue an // invoice at all, and discovering that at the moment of the first sale // is the worst possible time. $now = now(); foreach ([ ['invoice', 'Rechnung', 'RE'], ['credit_note', 'Gutschrift', 'GS'], ['cancellation', 'Storno', 'ST'], ['deposit', 'Anzahlung', 'AZ'], ] as [$kind, $name, $prefix]) { DB::table('invoice_series')->insert([ 'uuid' => (string) Str::uuid(), 'kind' => $kind, 'name' => $name, 'prefix' => $prefix, 'yearly_reset' => true, 'digits' => 4, 'next_number' => 1, 'counter_year' => null, 'active' => true, 'created_at' => $now, 'updated_at' => $now, ]); } } public function down(): void { Schema::dropIfExists('invoices'); Schema::dropIfExists('invoice_series'); } };