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 framework records this file when the body returns, and // that record is the only thing about this database that changes. 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.' ); } /** * 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'); } };