mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-09-03 05:41:07 +00:00
An upgraded install otherwise keeps 150 recorded rows (plus the one 2.4.x-only name) for migration files that no longer exist on disk. The SKIP path now deletes exactly the replaced set plus that superseded name, by explicit list only, before recording itself; module rows and any other foreign history survive. Found by the upgrade rehearsals.
1133 lines
50 KiB
PHP
1133 lines
50 KiB
PHP
<?php
|
|
|
|
use App\Platform\Operations\Database\SchemaConsolidationGuard;
|
|
use Illuminate\Database\Migrations\Migration;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
/**
|
|
* Collapses 150 historical migrations into one.
|
|
*
|
|
* Ten years of incremental schema changes used to run on every fresh install:
|
|
* tables created, columns added, renamed and dropped again, data back-filled
|
|
* for versions nobody runs any more. This file replaces all of it with the one
|
|
* state that chain converged on, so a new database reaches the 2.4.x boundary
|
|
* in a single step and the v3-era migrations take it from there.
|
|
*
|
|
* What it does depends entirely on what it finds. A database that already ran
|
|
* the replaced chain keeps every table it has — those are the real ones — and
|
|
* loses only the repository rows naming the files this one replaces, which this
|
|
* file has just made meaningless. Only an empty database is built. Anything in
|
|
* between is refused, loudly, before a single write: see
|
|
* {@see SchemaConsolidationGuard}.
|
|
*
|
|
* Everything below is portable builder work — no vendor SQL, no schema dumps.
|
|
* Two deliberate driver-specific behaviours are called out where they happen:
|
|
* the `after()` placements (a MySQL-only modifier) and the five legacy columns
|
|
* that only SQLite databases carry.
|
|
*/
|
|
return new class extends Migration
|
|
{
|
|
/**
|
|
* The version the replaced chain left in the settings table.
|
|
*/
|
|
private const BOUNDARY_VERSION = '1.3.0';
|
|
|
|
/**
|
|
* Columns SQLite databases kept that MySQL and PostgreSQL lost.
|
|
*
|
|
* Three of the replaced migrations dropped columns, and all three skipped
|
|
* the drop on SQLite because the SQLite of the day could not do it. A
|
|
* SQLite database built by the historical chain therefore still carries
|
|
* `users.company_id` and a `user_id` on four transaction tables, each with
|
|
* its foreign key intact. Reproducing them keeps a freshly built SQLite
|
|
* database identical to an upgraded one, which is the whole point of this
|
|
* file; the columns are unused either way.
|
|
*
|
|
* @var array<string, string> table => the column it kept
|
|
*/
|
|
private const SQLITE_RETAINED_COLUMNS = [
|
|
'users' => 'company_id',
|
|
'estimates' => 'user_id',
|
|
'expenses' => 'user_id',
|
|
'invoices' => 'user_id',
|
|
'payments' => 'user_id',
|
|
];
|
|
|
|
/**
|
|
* True when this database is one of the SQLite ones described above.
|
|
*/
|
|
private bool $retainsLegacyColumns = false;
|
|
|
|
public function up(): void
|
|
{
|
|
$verdict = SchemaConsolidationGuard::inspect();
|
|
|
|
if ($verdict->isAbort()) {
|
|
$verdict->fail();
|
|
}
|
|
|
|
if (! $verdict->isBuild()) {
|
|
// SKIP. The schema stands as it is; only the history it left
|
|
// behind changes. The framework records this file when the body
|
|
// returns, in place of the rows retired here.
|
|
$this->pruneStaleHistory();
|
|
|
|
return;
|
|
}
|
|
|
|
$this->retainsLegacyColumns = Schema::getConnection()->getDriverName() === 'sqlite';
|
|
|
|
$this->createStandaloneTables();
|
|
$this->createAccountTables();
|
|
$this->createCatalogTables();
|
|
$this->createContactTables();
|
|
$this->createSpendingTables();
|
|
$this->createBillingTables();
|
|
$this->createTaxTables();
|
|
$this->createAuthorizationTables();
|
|
$this->placeRelocatedColumns();
|
|
|
|
$this->seedFileDisks();
|
|
$this->seedCurrencies();
|
|
$this->seedVersion();
|
|
}
|
|
|
|
/**
|
|
* Consolidations are one-way.
|
|
*
|
|
* On an upgraded database the forward run did nothing at all, so a reverse
|
|
* run has no way to tell what it would be undoing — and on a fresh one it
|
|
* would mean dropping every table in the product.
|
|
*/
|
|
public function down(): void
|
|
{
|
|
throw new RuntimeException(
|
|
'The base schema consolidation is irreversible: on an upgraded database its forward run '
|
|
.'made no changes, so a rollback cannot know what to undo. Development environments should '
|
|
.'rebuild the database from scratch instead of rolling this migration back.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Retire the repository rows this file has just made meaningless.
|
|
*
|
|
* An upgraded database records one row for each of the 150 files replaced
|
|
* here, plus the single name the 2.4.x line shipped and this codebase never
|
|
* did. None of them can ever run again and none of them still names a file,
|
|
* so left in place they only make `migrate:status` describe a tree that
|
|
* stopped existing — and the next release would inherit the same 151 rows
|
|
* to explain away.
|
|
*
|
|
* The delete is driven by an exhaustive list of names, never by a date
|
|
* range or a prefix. Module migrations share this table, and so will every
|
|
* migration a later release adds; a row that is not named is a row that is
|
|
* not touched.
|
|
*
|
|
* The write goes to the connection the migration is running on, which the
|
|
* migrator has made the default for the duration of this method — the same
|
|
* connection the guard just read its verdict from.
|
|
*/
|
|
private function pruneStaleHistory(): void
|
|
{
|
|
Schema::getConnection()
|
|
->table(SchemaConsolidationGuard::repositoryTable())
|
|
->whereIn('migration', SchemaConsolidationGuard::staleRecordedMigrations())
|
|
->delete();
|
|
}
|
|
|
|
/**
|
|
* Tables that reference nothing, so nothing constrains their order.
|
|
*/
|
|
private function createStandaloneTables(): void
|
|
{
|
|
Schema::create('currencies', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('name');
|
|
$table->string('code');
|
|
$table->string('symbol')->nullable();
|
|
$table->integer('precision');
|
|
$table->string('thousand_separator');
|
|
$table->string('decimal_separator');
|
|
$table->boolean('swap_currency_symbol')->default(false);
|
|
$table->timestamps();
|
|
});
|
|
|
|
Schema::create('countries', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('code');
|
|
$table->string('name');
|
|
$table->integer('phonecode');
|
|
$table->index('id');
|
|
});
|
|
|
|
Schema::create('settings', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('option');
|
|
$table->string('value')->nullable();
|
|
$table->timestamps();
|
|
});
|
|
|
|
Schema::create('file_disks', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('name');
|
|
$table->string('type')->default('REMOTE');
|
|
$table->string('driver');
|
|
$table->boolean('set_as_default')->default(false);
|
|
$table->json('credentials');
|
|
$table->timestamps();
|
|
});
|
|
|
|
Schema::create('modules', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('name');
|
|
$table->string('version');
|
|
$table->boolean('installed')->default(false);
|
|
$table->boolean('enabled')->default(false);
|
|
$table->timestamps();
|
|
});
|
|
|
|
Schema::create('media', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('model_type');
|
|
$table->unsignedBigInteger('model_id');
|
|
$table->string('collection_name');
|
|
$table->string('name');
|
|
$table->string('file_name');
|
|
$table->string('mime_type')->nullable();
|
|
$table->string('disk');
|
|
$table->unsignedInteger('size');
|
|
$table->text('manipulations');
|
|
$table->text('custom_properties');
|
|
$table->text('responsive_images');
|
|
$table->unsignedInteger('order_column')->nullable();
|
|
$table->timestamps();
|
|
$table->uuid('uuid')->nullable();
|
|
$table->string('conversions_disk')->nullable();
|
|
$table->json('generated_conversions')->nullable();
|
|
$table->index(['model_type', 'model_id']);
|
|
});
|
|
|
|
Schema::create('notifications', function (Blueprint $table) {
|
|
$table->uuid('id')->primary();
|
|
$table->string('type');
|
|
$table->string('notifiable_type');
|
|
$table->unsignedBigInteger('notifiable_id');
|
|
$table->text('data');
|
|
$table->timestamp('read_at')->nullable();
|
|
$table->timestamps();
|
|
$table->index(['notifiable_type', 'notifiable_id']);
|
|
});
|
|
|
|
// Renamed from `password_resets` long after its index was named, and
|
|
// the index kept the old name. It has no primary key.
|
|
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
|
$table->string('email');
|
|
$table->string('token');
|
|
$table->timestamp('created_at')->nullable();
|
|
$table->index('email', 'password_resets_email_index');
|
|
});
|
|
|
|
Schema::create('cache', function (Blueprint $table) {
|
|
$table->string('key')->primary();
|
|
$table->mediumText('value');
|
|
$table->integer('expiration');
|
|
});
|
|
|
|
Schema::create('cache_locks', function (Blueprint $table) {
|
|
$table->string('key')->primary();
|
|
$table->string('owner');
|
|
$table->integer('expiration');
|
|
});
|
|
|
|
Schema::create('jobs', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('queue')->index();
|
|
$table->longText('payload');
|
|
$table->unsignedTinyInteger('attempts');
|
|
$table->unsignedInteger('reserved_at')->nullable();
|
|
$table->unsignedInteger('available_at');
|
|
$table->unsignedInteger('created_at');
|
|
});
|
|
|
|
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('tokenable_type');
|
|
$table->unsignedBigInteger('tokenable_id');
|
|
$table->string('name');
|
|
$table->string('token', 64)->unique();
|
|
$table->text('abilities')->nullable();
|
|
$table->timestamp('last_used_at')->nullable();
|
|
$table->timestamps();
|
|
$table->index(['tokenable_type', 'tokenable_id']);
|
|
});
|
|
|
|
Schema::create('email_logs', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('from');
|
|
$table->string('to');
|
|
$table->string('subject');
|
|
$table->text('body');
|
|
$table->string('mailable_type');
|
|
$table->string('mailable_id');
|
|
$table->timestamps();
|
|
$table->string('token')->nullable()->unique();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Users and companies, which reference each other in a loop.
|
|
*
|
|
* Users come first because `companies.owner_id` points at them. The other
|
|
* half of the loop only exists on SQLite, where a foreign key may name a
|
|
* table that does not exist yet — the constraint is resolved when rows are
|
|
* written, not when the table is declared.
|
|
*/
|
|
private function createAccountTables(): void
|
|
{
|
|
Schema::create('users', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('name');
|
|
$table->string('email')->nullable()->unique();
|
|
$table->string('phone')->nullable();
|
|
$table->string('password')->nullable();
|
|
$table->string('role')->default('user');
|
|
$table->rememberToken();
|
|
$table->string('facebook_id')->nullable();
|
|
$table->string('google_id')->nullable();
|
|
$table->string('github_id')->nullable();
|
|
$table->string('contact_name')->nullable();
|
|
$table->string('company_name')->nullable();
|
|
$table->string('website')->nullable();
|
|
$table->boolean('enable_portal')->nullable();
|
|
$table->unsignedInteger('currency_id')->nullable();
|
|
$this->addRetainedColumn($table, 'users');
|
|
$table->timestamps();
|
|
$table->unsignedInteger('creator_id')->nullable();
|
|
|
|
$table->foreign('currency_id')->references('id')->on('currencies')->onDelete('cascade');
|
|
$table->foreign('creator_id')->references('id')->on('users')->onDelete('cascade');
|
|
});
|
|
|
|
Schema::create('companies', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('name');
|
|
$table->string('logo')->nullable();
|
|
$table->string('unique_hash')->nullable();
|
|
$table->timestamps();
|
|
$table->string('slug')->nullable();
|
|
$table->unsignedInteger('owner_id')->nullable();
|
|
|
|
$table->foreign('owner_id')->references('id')->on('users');
|
|
});
|
|
|
|
Schema::create('user_company', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->unsignedInteger('user_id')->nullable();
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
|
|
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
});
|
|
|
|
Schema::create('user_settings', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('key');
|
|
$table->text('value');
|
|
$table->unsignedInteger('user_id');
|
|
$table->timestamps();
|
|
|
|
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
|
});
|
|
|
|
Schema::create('company_settings', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('option');
|
|
$table->text('value');
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies');
|
|
});
|
|
|
|
Schema::create('custom_fields', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('name');
|
|
$table->string('slug');
|
|
$table->string('label');
|
|
$table->string('model_type');
|
|
$table->string('type');
|
|
$table->string('placeholder')->nullable();
|
|
$table->json('options')->nullable();
|
|
$table->boolean('boolean_answer')->nullable();
|
|
$table->date('date_answer')->nullable();
|
|
$table->time('time_answer')->nullable();
|
|
$table->text('string_answer')->nullable();
|
|
$table->unsignedBigInteger('number_answer')->nullable();
|
|
$table->dateTime('date_time_answer')->nullable();
|
|
$table->boolean('is_required')->default(false);
|
|
$table->unsignedBigInteger('order')->default(1);
|
|
$table->unsignedInteger('company_id');
|
|
$table->timestamps();
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies');
|
|
});
|
|
|
|
Schema::create('custom_field_values', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('custom_field_valuable_type');
|
|
$table->unsignedInteger('custom_field_valuable_id');
|
|
$table->string('type');
|
|
$table->boolean('boolean_answer')->nullable();
|
|
$table->date('date_answer')->nullable();
|
|
$table->time('time_answer')->nullable();
|
|
$table->text('string_answer')->nullable();
|
|
$table->unsignedBigInteger('number_answer')->nullable();
|
|
$table->dateTime('date_time_answer')->nullable();
|
|
$table->unsignedBigInteger('custom_field_id');
|
|
$table->unsignedInteger('company_id');
|
|
$table->timestamps();
|
|
|
|
$table->foreign('custom_field_id')->references('id')->on('custom_fields');
|
|
$table->foreign('company_id')->references('id')->on('companies');
|
|
});
|
|
|
|
Schema::create('notes', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('type');
|
|
$table->string('name');
|
|
$table->text('notes');
|
|
$table->timestamps();
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->boolean('is_default')->default(false);
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
});
|
|
|
|
Schema::create('exchange_rate_logs', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->unsignedInteger('base_currency_id')->nullable();
|
|
$table->unsignedInteger('currency_id')->nullable();
|
|
$table->decimal('exchange_rate', 19, 6)->nullable();
|
|
$table->timestamps();
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies');
|
|
$table->foreign('base_currency_id')->references('id')->on('currencies');
|
|
$table->foreign('currency_id')->references('id')->on('currencies');
|
|
});
|
|
|
|
Schema::create('exchange_rate_providers', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('driver');
|
|
$table->string('key');
|
|
$table->json('currencies')->nullable();
|
|
$table->json('driver_config')->nullable();
|
|
$table->boolean('active')->default(true);
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* What the business sells.
|
|
*/
|
|
private function createCatalogTables(): void
|
|
{
|
|
Schema::create('units', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('name');
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
});
|
|
|
|
Schema::create('items', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('name');
|
|
$table->text('description')->nullable();
|
|
$table->unsignedBigInteger('price');
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->unsignedInteger('unit_id')->nullable();
|
|
$table->timestamps();
|
|
$table->unsignedInteger('creator_id')->nullable();
|
|
$table->unsignedInteger('currency_id')->nullable();
|
|
$table->boolean('tax_per_item')->default(false);
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
$table->foreign('unit_id')->references('id')->on('units')->onDelete('cascade');
|
|
$table->foreign('creator_id')->references('id')->on('users')->onDelete('cascade');
|
|
$table->foreign('currency_id')->references('id')->on('currencies');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Who the business sells to.
|
|
*/
|
|
private function createContactTables(): void
|
|
{
|
|
Schema::create('customers', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('name');
|
|
$table->string('email')->nullable();
|
|
$table->string('phone')->nullable();
|
|
$table->string('password')->nullable();
|
|
$table->rememberToken();
|
|
$table->string('facebook_id')->nullable();
|
|
$table->string('google_id')->nullable();
|
|
$table->string('github_id')->nullable();
|
|
$table->string('contact_name')->nullable();
|
|
$table->string('company_name')->nullable();
|
|
$table->string('website')->nullable();
|
|
$table->boolean('enable_portal')->default(false);
|
|
$table->unsignedInteger('currency_id')->nullable();
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->unsignedInteger('creator_id')->nullable();
|
|
$table->timestamps();
|
|
|
|
$table->foreign('currency_id')->references('id')->on('currencies');
|
|
$table->foreign('company_id')->references('id')->on('companies');
|
|
$table->foreign('creator_id')->references('id')->on('users');
|
|
});
|
|
|
|
Schema::create('addresses', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('name')->nullable();
|
|
$table->string('address_street_1')->nullable();
|
|
$table->string('address_street_2')->nullable();
|
|
$table->string('city')->nullable();
|
|
$table->string('state')->nullable();
|
|
$table->unsignedInteger('country_id')->nullable();
|
|
$table->string('zip')->nullable();
|
|
$table->string('phone')->nullable();
|
|
$table->string('fax')->nullable();
|
|
$table->string('type')->nullable();
|
|
$table->unsignedInteger('user_id')->nullable();
|
|
$table->timestamps();
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->unsignedBigInteger('customer_id')->nullable();
|
|
|
|
$table->foreign('country_id')->references('id')->on('countries');
|
|
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
$table->foreign('customer_id')->references('id')->on('customers');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Money going out.
|
|
*/
|
|
private function createSpendingTables(): void
|
|
{
|
|
Schema::create('expense_categories', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('name');
|
|
$table->string('description')->nullable();
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
});
|
|
|
|
Schema::create('payment_methods', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('name');
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
$table->string('driver')->nullable();
|
|
$table->enum('type', ['GENERAL', 'MODULE'])->default('GENERAL');
|
|
$table->json('settings')->nullable();
|
|
$table->boolean('active')->default(false);
|
|
$table->boolean('use_test_env')->default(false);
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
});
|
|
|
|
// `customer_id` and `currency_id` are plain indexed-free columns here:
|
|
// the relationship is enforced in application code, not by the database.
|
|
Schema::create('expenses', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->date('expense_date');
|
|
$table->string('attachment_receipt')->nullable();
|
|
$table->unsignedBigInteger('amount');
|
|
$table->text('notes')->nullable();
|
|
$table->unsignedInteger('expense_category_id');
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
$this->addRetainedColumn($table, 'expenses');
|
|
$table->unsignedInteger('creator_id')->nullable();
|
|
$table->unsignedBigInteger('customer_id')->nullable();
|
|
$table->decimal('exchange_rate', 19, 6)->nullable();
|
|
$table->unsignedBigInteger('base_amount')->nullable();
|
|
$table->unsignedInteger('currency_id')->nullable();
|
|
$table->unsignedInteger('payment_method_id')->nullable();
|
|
|
|
$table->foreign('expense_category_id')->references('id')->on('expense_categories')->onDelete('cascade');
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
$table->foreign('creator_id')->references('id')->on('users')->onDelete('cascade');
|
|
$table->foreign('payment_method_id')->references('id')->on('payment_methods');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Money coming in.
|
|
*/
|
|
private function createBillingTables(): void
|
|
{
|
|
Schema::create('recurring_invoices', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->dateTime('starts_at');
|
|
$table->boolean('send_automatically')->default(false);
|
|
$table->unsignedBigInteger('customer_id')->nullable();
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->enum('status', ['COMPLETED', 'ON_HOLD', 'ACTIVE'])->default('ACTIVE');
|
|
$table->dateTime('next_invoice_at')->nullable();
|
|
$table->unsignedInteger('creator_id')->nullable();
|
|
$table->string('frequency');
|
|
$table->enum('limit_by', ['NONE', 'COUNT', 'DATE'])->default('NONE');
|
|
$table->integer('limit_count')->nullable();
|
|
$table->date('limit_date')->nullable();
|
|
$table->unsignedInteger('currency_id')->nullable();
|
|
$table->decimal('exchange_rate', 19, 6)->nullable();
|
|
$table->string('tax_per_item');
|
|
$table->string('discount_per_item');
|
|
$table->text('notes')->nullable();
|
|
$table->string('discount_type')->nullable();
|
|
$table->decimal('discount', 15, 2)->nullable();
|
|
$table->unsignedBigInteger('discount_val')->nullable();
|
|
$table->unsignedBigInteger('sub_total');
|
|
$table->unsignedBigInteger('total');
|
|
$table->unsignedBigInteger('tax');
|
|
$table->string('template_name')->nullable();
|
|
$table->unsignedBigInteger('due_amount');
|
|
$table->timestamps();
|
|
$table->string('sales_tax_type')->nullable();
|
|
$table->string('sales_tax_address_type')->nullable();
|
|
$table->boolean('tax_included')->default(false);
|
|
|
|
$table->foreign('customer_id')->references('id')->on('customers');
|
|
$table->foreign('company_id')->references('id')->on('companies');
|
|
$table->foreign('creator_id')->references('id')->on('users');
|
|
$table->foreign('currency_id')->references('id')->on('currencies');
|
|
});
|
|
|
|
// The money columns here are signed: credit-style invoices are allowed
|
|
// to carry negative totals.
|
|
Schema::create('invoices', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->dateTime('invoice_date');
|
|
$table->date('due_date')->nullable();
|
|
$table->string('invoice_number');
|
|
$table->string('reference_number')->nullable();
|
|
$table->string('status');
|
|
$table->string('paid_status');
|
|
$table->string('tax_per_item');
|
|
$table->string('discount_per_item');
|
|
$table->text('notes')->nullable();
|
|
$table->string('discount_type')->nullable();
|
|
$table->decimal('discount', 15, 2)->nullable();
|
|
$table->bigInteger('discount_val')->nullable();
|
|
$table->bigInteger('sub_total');
|
|
$table->bigInteger('total');
|
|
$table->bigInteger('tax');
|
|
$table->bigInteger('due_amount');
|
|
$table->boolean('sent')->default(false);
|
|
$table->boolean('viewed')->default(false);
|
|
$table->string('unique_hash')->nullable();
|
|
$this->addRetainedColumn($table, 'invoices');
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
$table->unsignedInteger('creator_id')->nullable();
|
|
$table->string('template_name')->nullable();
|
|
$table->unsignedBigInteger('customer_id')->nullable();
|
|
$table->unsignedBigInteger('recurring_invoice_id')->nullable();
|
|
$table->decimal('exchange_rate', 19, 6)->nullable();
|
|
$table->bigInteger('base_discount_val')->nullable();
|
|
$table->bigInteger('base_sub_total')->nullable();
|
|
$table->bigInteger('base_total')->nullable();
|
|
$table->bigInteger('base_tax')->nullable();
|
|
$table->bigInteger('base_due_amount')->nullable();
|
|
$table->unsignedInteger('currency_id')->nullable();
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
$table->foreign('creator_id')->references('id')->on('users')->onDelete('cascade');
|
|
$table->foreign('customer_id')->references('id')->on('customers');
|
|
$table->foreign('recurring_invoice_id')->references('id')->on('recurring_invoices');
|
|
$table->foreign('currency_id')->references('id')->on('currencies');
|
|
});
|
|
|
|
Schema::create('invoice_items', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('name');
|
|
$table->text('description')->nullable();
|
|
$table->string('discount_type');
|
|
$table->bigInteger('price');
|
|
$table->decimal('quantity', 15, 2);
|
|
$table->decimal('discount', 15, 2)->nullable();
|
|
$table->bigInteger('discount_val');
|
|
$table->bigInteger('tax');
|
|
$table->bigInteger('total');
|
|
$table->unsignedInteger('invoice_id')->nullable();
|
|
$table->unsignedInteger('item_id')->nullable();
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
|
|
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
|
|
$table->foreign('item_id')->references('id')->on('items')->onDelete('cascade');
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
});
|
|
|
|
Schema::create('estimates', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->date('estimate_date');
|
|
$table->date('expiry_date')->nullable();
|
|
$table->string('estimate_number');
|
|
$table->string('status');
|
|
$table->string('reference_number')->nullable();
|
|
$table->string('tax_per_item');
|
|
$table->string('discount_per_item');
|
|
$table->text('notes')->nullable();
|
|
$table->decimal('discount', 15, 2)->nullable();
|
|
$table->string('discount_type')->nullable();
|
|
$table->unsignedBigInteger('discount_val')->nullable();
|
|
$table->unsignedBigInteger('sub_total');
|
|
$table->unsignedBigInteger('total');
|
|
$table->unsignedBigInteger('tax');
|
|
$table->string('unique_hash')->nullable();
|
|
$this->addRetainedColumn($table, 'estimates');
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
$table->unsignedInteger('creator_id')->nullable();
|
|
$table->string('template_name')->nullable();
|
|
$table->unsignedBigInteger('customer_id')->nullable();
|
|
$table->decimal('exchange_rate', 19, 6)->nullable();
|
|
$table->unsignedBigInteger('base_discount_val')->nullable();
|
|
$table->unsignedBigInteger('base_sub_total')->nullable();
|
|
$table->unsignedBigInteger('base_total')->nullable();
|
|
$table->unsignedBigInteger('base_tax')->nullable();
|
|
$table->unsignedInteger('currency_id')->nullable();
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
$table->foreign('creator_id')->references('id')->on('users')->onDelete('cascade');
|
|
$table->foreign('customer_id')->references('id')->on('customers');
|
|
$table->foreign('currency_id')->references('id')->on('currencies');
|
|
});
|
|
|
|
Schema::create('estimate_items', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('name');
|
|
$table->text('description')->nullable();
|
|
$table->string('discount_type');
|
|
$table->decimal('quantity', 15, 2);
|
|
$table->decimal('discount', 15, 2)->nullable();
|
|
$table->unsignedBigInteger('discount_val')->nullable();
|
|
$table->unsignedBigInteger('price');
|
|
$table->unsignedBigInteger('tax');
|
|
$table->unsignedBigInteger('total');
|
|
$table->unsignedInteger('item_id')->nullable();
|
|
$table->unsignedInteger('estimate_id');
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
|
|
$table->foreign('item_id')->references('id')->on('items')->onDelete('cascade');
|
|
$table->foreign('estimate_id')->references('id')->on('estimates')->onDelete('cascade');
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
});
|
|
|
|
Schema::create('transactions', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('transaction_id')->nullable();
|
|
$table->string('unique_hash')->nullable();
|
|
$table->string('type')->nullable();
|
|
$table->string('status');
|
|
$table->dateTime('transaction_date');
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->unsignedInteger('invoice_id');
|
|
$table->timestamps();
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies');
|
|
$table->foreign('invoice_id')->references('id')->on('invoices');
|
|
});
|
|
|
|
Schema::create('payments', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('payment_number');
|
|
$table->date('payment_date');
|
|
$table->text('notes')->nullable();
|
|
$table->unsignedBigInteger('amount');
|
|
$table->string('unique_hash')->nullable();
|
|
$this->addRetainedColumn($table, 'payments');
|
|
$table->unsignedInteger('invoice_id')->nullable();
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->unsignedInteger('payment_method_id')->nullable();
|
|
$table->timestamps();
|
|
$table->unsignedInteger('creator_id')->nullable();
|
|
$table->unsignedBigInteger('customer_id')->nullable();
|
|
$table->decimal('exchange_rate', 19, 6)->nullable();
|
|
$table->unsignedBigInteger('base_amount')->nullable();
|
|
$table->unsignedInteger('currency_id')->nullable();
|
|
|
|
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
|
|
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
|
$table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade');
|
|
$table->foreign('creator_id')->references('id')->on('users')->onDelete('cascade');
|
|
$table->foreign('customer_id')->references('id')->on('customers');
|
|
$table->foreign('currency_id')->references('id')->on('currencies');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Tax definitions and the tax lines they produce.
|
|
*/
|
|
private function createTaxTables(): void
|
|
{
|
|
Schema::create('tax_types', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->string('name');
|
|
$table->decimal('percent', 5, 2)->nullable();
|
|
$table->tinyInteger('compound_tax')->default(0);
|
|
$table->tinyInteger('collective_tax')->default(0);
|
|
$table->text('description')->nullable();
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->timestamps();
|
|
$table->enum('type', ['GENERAL', 'MODULE'])->default('GENERAL');
|
|
|
|
$table->foreign('company_id')->references('id')->on('companies');
|
|
});
|
|
|
|
// Every document type that can carry tax points here, which is why the
|
|
// table has nine foreign keys and eight nullable owners.
|
|
Schema::create('taxes', function (Blueprint $table) {
|
|
$table->increments('id');
|
|
$table->unsignedInteger('tax_type_id');
|
|
$table->unsignedInteger('invoice_id')->nullable();
|
|
$table->unsignedInteger('estimate_id')->nullable();
|
|
$table->unsignedInteger('invoice_item_id')->nullable();
|
|
$table->unsignedInteger('estimate_item_id')->nullable();
|
|
$table->unsignedInteger('item_id')->nullable();
|
|
$table->unsignedInteger('company_id')->nullable();
|
|
$table->string('name');
|
|
$table->bigInteger('amount');
|
|
$table->decimal('percent', 5, 2)->nullable();
|
|
$table->tinyInteger('compound_tax')->default(0);
|
|
$table->timestamps();
|
|
$table->decimal('exchange_rate', 19, 6)->nullable();
|
|
$table->unsignedBigInteger('base_amount')->nullable();
|
|
$table->unsignedInteger('currency_id')->nullable();
|
|
$table->unsignedBigInteger('recurring_invoice_id')->nullable();
|
|
|
|
$table->foreign('tax_type_id')->references('id')->on('tax_types');
|
|
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
|
|
$table->foreign('estimate_id')->references('id')->on('estimates')->onDelete('cascade');
|
|
$table->foreign('invoice_item_id')->references('id')->on('invoice_items')->onDelete('cascade');
|
|
$table->foreign('estimate_item_id')->references('id')->on('estimate_items')->onDelete('cascade');
|
|
$table->foreign('item_id')->references('id')->on('items')->onDelete('cascade');
|
|
$table->foreign('company_id')->references('id')->on('companies');
|
|
$table->foreign('currency_id')->references('id')->on('currencies');
|
|
$table->foreign('recurring_invoice_id')->references('id')->on('recurring_invoices');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Bouncer's four tables.
|
|
*
|
|
* The explicit index on each foreign-key column is deliberate: MySQL
|
|
* reuses it for the constraint instead of creating a second, differently
|
|
* named one, which is what the boundary schema records.
|
|
*/
|
|
private function createAuthorizationTables(): void
|
|
{
|
|
Schema::create('abilities', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('name');
|
|
$table->string('title')->nullable();
|
|
$table->unsignedBigInteger('entity_id')->nullable();
|
|
$table->string('entity_type')->nullable();
|
|
$table->boolean('only_owned')->default(false);
|
|
$table->json('options')->nullable();
|
|
$table->integer('scope')->nullable()->index();
|
|
$table->timestamps();
|
|
});
|
|
|
|
Schema::create('roles', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->string('name');
|
|
$table->string('title')->nullable();
|
|
$table->unsignedInteger('level')->nullable();
|
|
$table->integer('scope')->nullable()->index();
|
|
$table->timestamps();
|
|
|
|
$table->unique(['name', 'scope'], 'roles_name_unique');
|
|
});
|
|
|
|
Schema::create('assigned_roles', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->unsignedBigInteger('role_id');
|
|
$table->unsignedBigInteger('entity_id');
|
|
$table->string('entity_type');
|
|
$table->unsignedBigInteger('restricted_to_id')->nullable();
|
|
$table->string('restricted_to_type')->nullable();
|
|
$table->integer('scope')->nullable()->index();
|
|
|
|
$table->index(['entity_id', 'entity_type', 'scope'], 'assigned_roles_entity_index');
|
|
$table->index('role_id', 'assigned_roles_role_id_index');
|
|
$table->foreign('role_id')->references('id')->on('roles')
|
|
->onUpdate('cascade')->onDelete('cascade');
|
|
});
|
|
|
|
Schema::create('permissions', function (Blueprint $table) {
|
|
$table->bigIncrements('id');
|
|
$table->unsignedBigInteger('ability_id');
|
|
$table->unsignedBigInteger('entity_id')->nullable();
|
|
$table->string('entity_type')->nullable();
|
|
$table->boolean('forbidden')->default(false);
|
|
$table->integer('scope')->nullable()->index();
|
|
|
|
$table->index(['entity_id', 'entity_type', 'scope'], 'permissions_entity_index');
|
|
$table->index('ability_id', 'permissions_ability_id_index');
|
|
$table->foreign('ability_id')->references('id')->on('abilities')
|
|
->onUpdate('cascade')->onDelete('cascade');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Columns that MySQL keeps somewhere other than the end of the table.
|
|
*
|
|
* The replaced chain added these after the fact, and MySQL honours the
|
|
* `AFTER` clause while PostgreSQL and SQLite always append. Adding them in
|
|
* a second pass reproduces both layouts from one body of code: `after()`
|
|
* places them on MySQL and is ignored everywhere else, where they land in
|
|
* this method's own order. Column order carries no meaning to the
|
|
* application — it is reproduced so that a database built here and one
|
|
* upgraded from 2.x are physically the same.
|
|
*/
|
|
private function placeRelocatedColumns(): void
|
|
{
|
|
Schema::table('companies', function (Blueprint $table) {
|
|
$table->string('vat_id')->nullable()->after('unique_hash');
|
|
$table->string('tax_id')->nullable()->after('vat_id');
|
|
});
|
|
|
|
Schema::table('customers', function (Blueprint $table) {
|
|
$table->string('prefix')->nullable()->after('id');
|
|
$table->string('tax_id')->nullable()->after('github_id');
|
|
});
|
|
|
|
Schema::table('expenses', function (Blueprint $table) {
|
|
$table->string('expense_number')->nullable()->after('expense_date');
|
|
$table->index('expense_number');
|
|
});
|
|
|
|
Schema::table('personal_access_tokens', function (Blueprint $table) {
|
|
$table->dateTime('expires_at')->nullable()->after('last_used_at');
|
|
});
|
|
|
|
Schema::table('tax_types', function (Blueprint $table) {
|
|
$table->enum('calculation_type', ['percentage', 'fixed'])->default('percentage')->after('name');
|
|
$table->integer('fixed_amount')->nullable()->after('percent');
|
|
});
|
|
|
|
Schema::table('taxes', function (Blueprint $table) {
|
|
$table->enum('calculation_type', ['percentage', 'fixed'])->default('percentage')->after('name');
|
|
$table->integer('fixed_amount')->nullable()->after('percent');
|
|
});
|
|
|
|
Schema::table('estimates', function (Blueprint $table) {
|
|
$table->unsignedMediumInteger('sequence_number')->nullable()->after('id');
|
|
$table->unsignedMediumInteger('customer_sequence_number')->nullable()->after('sequence_number');
|
|
$table->string('sales_tax_type')->nullable()->after('currency_id');
|
|
$table->string('sales_tax_address_type')->nullable()->after('sales_tax_type');
|
|
$table->boolean('tax_included')->default(false)->after('sales_tax_address_type');
|
|
});
|
|
|
|
Schema::table('estimate_items', function (Blueprint $table) {
|
|
$table->string('unit_name')->nullable()->after('quantity');
|
|
$table->decimal('exchange_rate', 19, 6)->nullable()->after('updated_at');
|
|
$table->unsignedBigInteger('base_discount_val')->nullable()->after('exchange_rate');
|
|
$table->unsignedBigInteger('base_price')->nullable()->after('base_discount_val');
|
|
$table->unsignedBigInteger('base_tax')->nullable()->after('base_price');
|
|
$table->unsignedBigInteger('base_total')->nullable()->after('base_tax');
|
|
});
|
|
|
|
Schema::table('invoices', function (Blueprint $table) {
|
|
$table->unsignedMediumInteger('sequence_number')->nullable()->after('id');
|
|
$table->unsignedMediumInteger('customer_sequence_number')->nullable()->after('sequence_number');
|
|
$table->string('sales_tax_type')->nullable()->after('currency_id');
|
|
$table->string('sales_tax_address_type')->nullable()->after('sales_tax_type');
|
|
$table->boolean('overdue')->default(false)->after('sales_tax_address_type');
|
|
$table->boolean('tax_included')->default(false)->after('overdue');
|
|
});
|
|
|
|
Schema::table('invoice_items', function (Blueprint $table) {
|
|
$table->string('unit_name')->nullable()->after('quantity');
|
|
$table->unsignedBigInteger('recurring_invoice_id')->nullable()->after('updated_at');
|
|
$table->unsignedBigInteger('base_price')->nullable()->after('recurring_invoice_id');
|
|
$table->decimal('exchange_rate', 19, 6)->nullable()->after('base_price');
|
|
$table->bigInteger('base_discount_val')->nullable()->after('exchange_rate');
|
|
$table->bigInteger('base_tax')->nullable()->after('base_discount_val');
|
|
$table->bigInteger('base_total')->nullable()->after('base_tax');
|
|
|
|
$table->foreign('recurring_invoice_id')->references('id')->on('recurring_invoices');
|
|
});
|
|
|
|
Schema::table('payments', function (Blueprint $table) {
|
|
$table->unsignedMediumInteger('sequence_number')->nullable()->after('id');
|
|
$table->unsignedMediumInteger('customer_sequence_number')->nullable()->after('sequence_number');
|
|
$table->unsignedBigInteger('transaction_id')->nullable()->after('currency_id');
|
|
|
|
$table->foreign('transaction_id')->references('id')->on('transactions');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The two disks every installation starts with.
|
|
*
|
|
* Both are local, and both take their paths from the running application
|
|
* rather than from anything baked into this file, so an installation moved
|
|
* to a different directory still describes itself correctly.
|
|
*/
|
|
private function seedFileDisks(): void
|
|
{
|
|
$now = now();
|
|
|
|
DB::table('file_disks')->insert([
|
|
[
|
|
'name' => 'public',
|
|
'type' => 'SYSTEM',
|
|
'driver' => 'local',
|
|
'set_as_default' => false,
|
|
'credentials' => json_encode([
|
|
'driver' => 'local',
|
|
'root' => config('filesystems.disks.public.root'),
|
|
'url' => config('app.url').'/storage',
|
|
'visibility' => 'public',
|
|
]),
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
],
|
|
[
|
|
'name' => 'local_private',
|
|
'type' => 'SYSTEM',
|
|
'driver' => 'local',
|
|
'set_as_default' => true,
|
|
'credentials' => json_encode([
|
|
'root' => config('filesystems.disks.local.root'),
|
|
'driver' => 'local',
|
|
]),
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Three currencies the replaced chain added after its first release.
|
|
*
|
|
* Each is written only when its code is absent, so a database that already
|
|
* knows one of them — because a seeder or a module got there first — keeps
|
|
* exactly one row for it.
|
|
*/
|
|
private function seedCurrencies(): void
|
|
{
|
|
$now = now();
|
|
|
|
$currencies = [
|
|
[
|
|
'code' => 'DZD',
|
|
'name' => 'Algerian Dinar',
|
|
'symbol' => 'DA',
|
|
'precision' => 2,
|
|
'thousand_separator' => ',',
|
|
'decimal_separator' => '.',
|
|
],
|
|
[
|
|
'code' => 'PYG',
|
|
'name' => 'Paraguayan Guaraní',
|
|
'symbol' => '₲',
|
|
'precision' => 0,
|
|
'thousand_separator' => '.',
|
|
'decimal_separator' => ',',
|
|
],
|
|
[
|
|
'code' => 'QAR',
|
|
'name' => 'Qatari Riyal',
|
|
'symbol' => 'QR',
|
|
'precision' => 2,
|
|
'thousand_separator' => ',',
|
|
'decimal_separator' => '.',
|
|
],
|
|
];
|
|
|
|
foreach ($currencies as $currency) {
|
|
$alreadyKnown = DB::table('currencies')
|
|
->where('code', $currency['code'])
|
|
->exists();
|
|
|
|
if ($alreadyKnown) {
|
|
continue;
|
|
}
|
|
|
|
DB::table('currencies')->insert($currency + [
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stamp the version the replaced chain ended on.
|
|
*
|
|
* The v3-era migrations move it forward from here; what matters is that the
|
|
* row exists and reads as the boundary version, whether or not something
|
|
* wrote one first.
|
|
*/
|
|
private function seedVersion(): void
|
|
{
|
|
$now = now();
|
|
|
|
$existing = DB::table('settings')->where('option', 'version')->exists();
|
|
|
|
if ($existing) {
|
|
DB::table('settings')
|
|
->where('option', 'version')
|
|
->update([
|
|
'value' => self::BOUNDARY_VERSION,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
return;
|
|
}
|
|
|
|
DB::table('settings')->insert([
|
|
'option' => 'version',
|
|
'value' => self::BOUNDARY_VERSION,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Declare one of the SQLite-only legacy columns, if this is SQLite.
|
|
*
|
|
* Called from inside the table's own closure so the column lands in the
|
|
* position the historical chain left it in.
|
|
*/
|
|
private function addRetainedColumn(Blueprint $table, string $forTable): void
|
|
{
|
|
if (! $this->retainsLegacyColumns) {
|
|
return;
|
|
}
|
|
|
|
$column = self::SQLITE_RETAINED_COLUMNS[$forTable];
|
|
$references = $column === 'company_id' ? 'companies' : 'users';
|
|
|
|
$table->unsignedInteger($column)->nullable();
|
|
$table->foreign($column)->references('id')->on($references)->onDelete('cascade');
|
|
}
|
|
};
|