Giant Update

This commit is contained in:
Wian Drs
2026-07-31 13:57:11 +07:00
parent f68d967c8b
commit b0d08211ec
112 changed files with 6674 additions and 3 deletions
@@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('customer_code');
$table->string('name');
$table->string('identity_number', 100)->nullable();
$table->string('email')->nullable();
$table->string('whatsapp_number', 50)->nullable();
$table->text('installation_address')->nullable();
$table->foreignId('provinsi_id')->nullable()->constrained('wilayah_provinsi')->nullOnDelete();
$table->foreignId('kabupaten_id')->nullable()->constrained('wilayah_kabupaten')->nullOnDelete();
$table->foreignId('kecamatan_id')->nullable()->constrained('wilayah_kecamatan')->nullOnDelete();
$table->foreignId('desa_id')->nullable()->constrained('wilayah_desa')->nullOnDelete();
$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
$table->foreignId('package_profile_id')->nullable()->constrained('nas_package_profiles')->nullOnDelete();
$table->foreignId('nas_mikrotik_id')->nullable()->constrained('nas_mikrotiks')->nullOnDelete();
$table->unsignedBigInteger('billing_profile_id')->nullable()->index();
$table->unsignedBigInteger('topology_id')->nullable()->index();
$table->string('external_username')->nullable();
$table->string('source', 20)->default('manual');
$table->string('status', 20)->default('order');
$table->string('order_stage', 30)->default('registration');
$table->timestamp('activated_at')->nullable();
$table->timestamp('inactivated_at')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['tenant_id', 'customer_code']);
$table->index(['tenant_id', 'status']);
$table->index(['tenant_id', 'source', 'external_username']);
});
}
public function down(): void
{
Schema::dropIfExists('customers');
}
};
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('customer_images', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->foreignId('stored_file_id')->constrained('stored_files')->cascadeOnDelete();
$table->string('category', 30)->default('other');
$table->string('caption')->nullable();
$table->boolean('is_cover')->default(false);
$table->unsignedSmallInteger('sort_order')->default(0);
$table->timestamps();
$table->unique(['customer_id', 'stored_file_id']);
$table->index(['customer_id', 'is_cover']);
});
}
public function down(): void
{
Schema::dropIfExists('customer_images');
}
};
@@ -0,0 +1,89 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('billing_profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->unsignedTinyInteger('invoice_day');
$table->unsignedTinyInteger('send_day');
$table->unsignedTinyInteger('warning_day');
$table->unsignedTinyInteger('isolation_day');
$table->string('tax_type', 20)->default('non_tax');
$table->decimal('tax_rate', 5, 2)->default(0);
$table->string('status', 20)->default('active');
$table->text('notes')->nullable();
$table->timestamps();
$table->unique(['tenant_id', 'name']);
$table->index(['tenant_id', 'status']);
});
Schema::table('customers', function (Blueprint $table) {
$table->foreign('billing_profile_id')
->references('id')
->on('billing_profiles')
->nullOnDelete();
});
Schema::create('invoices', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('customer_id')->constrained()->restrictOnDelete();
$table->foreignId('billing_profile_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('paid_by')->nullable()->constrained('users')->nullOnDelete();
$table->string('invoice_number');
$table->date('period_start');
$table->date('period_end');
$table->date('issue_date');
$table->date('send_date')->nullable();
$table->date('warning_date')->nullable();
$table->date('isolation_date')->nullable();
$table->decimal('subtotal', 15, 2);
$table->string('tax_type', 20)->default('non_tax');
$table->decimal('tax_rate', 5, 2)->default(0);
$table->decimal('tax_amount', 15, 2)->default(0);
$table->decimal('total', 15, 2);
$table->decimal('paid_amount', 15, 2)->default(0);
$table->decimal('balance', 15, 2);
$table->string('status', 20)->default('draft');
$table->string('payment_status', 20)->default('unpaid');
$table->timestamp('paid_at')->nullable();
$table->string('payment_method', 50)->nullable();
$table->string('payment_reference')->nullable();
$table->string('external_payment_id')->nullable()->index();
$table->string('finance_reference')->nullable()->index();
$table->string('notification_status', 30)->default('pending');
$table->string('customer_code_snapshot');
$table->string('customer_name_snapshot');
$table->text('customer_address_snapshot')->nullable();
$table->string('package_name_snapshot')->nullable();
$table->json('profile_snapshot')->nullable();
$table->json('metadata')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['tenant_id', 'invoice_number']);
$table->index(['tenant_id', 'status', 'issue_date']);
$table->index(['tenant_id', 'payment_status', 'isolation_date']);
$table->index(['customer_id', 'period_start', 'period_end']);
});
}
public function down(): void
{
Schema::dropIfExists('invoices');
Schema::table('customers', function (Blueprint $table) {
$table->dropForeign(['billing_profile_id']);
});
Schema::dropIfExists('billing_profiles');
}
};
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('billing_profiles', function (Blueprint $table) {
$table->string('schedule_type', 30)->default('fixed_date')->after('name');
$table->unsignedTinyInteger('invoice_days_before')->nullable()->after('isolation_day');
$table->unsignedTinyInteger('send_days_before')->nullable()->after('invoice_days_before');
$table->unsignedTinyInteger('warning_days_before')->nullable()->after('send_days_before');
$table->unsignedTinyInteger('isolation_days_after')->nullable()->after('warning_days_before');
$table->index(['tenant_id', 'schedule_type']);
});
}
public function down(): void
{
Schema::table('billing_profiles', function (Blueprint $table) {
$table->dropIndex(['tenant_id', 'schedule_type']);
$table->dropColumn([
'schedule_type',
'invoice_days_before',
'send_days_before',
'warning_days_before',
'isolation_days_after',
]);
});
}
};
@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->unique(
['customer_id', 'period_start', 'period_end'],
'invoices_customer_period_unique',
);
});
}
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->dropUnique('invoices_customer_period_unique');
});
}
};
@@ -0,0 +1,153 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('topology_device_types', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained()->cascadeOnDelete();
$table->string('code', 50);
$table->string('name');
$table->string('category', 50);
$table->boolean('can_have_ports')->default(true);
$table->boolean('can_have_splitters')->default(false);
$table->boolean('is_system')->default(false);
$table->string('icon')->nullable();
$table->string('color', 20)->default('#2f74c0');
$table->string('status', 20)->default('active');
$table->timestamps();
$table->unique(['tenant_id', 'code']);
$table->index(['tenant_id', 'status']);
});
Schema::create('topology_nodes', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('device_type_id')->constrained('topology_device_types')->restrictOnDelete();
$table->foreignId('parent_node_id')->nullable()->constrained('topology_nodes')->nullOnDelete();
$table->foreignId('provinsi_id')->nullable()->constrained('wilayah_provinsi')->nullOnDelete();
$table->foreignId('kabupaten_id')->nullable()->constrained('wilayah_kabupaten')->nullOnDelete();
$table->foreignId('kecamatan_id')->nullable()->constrained('wilayah_kecamatan')->nullOnDelete();
$table->foreignId('desa_id')->nullable()->constrained('wilayah_desa')->nullOnDelete();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
$table->string('code');
$table->string('name');
$table->text('description')->nullable();
$table->string('status', 30)->default('active');
$table->decimal('latitude', 10, 7);
$table->decimal('longitude', 10, 7);
$table->text('address')->nullable();
$table->unsignedInteger('capacity')->nullable();
$table->date('installed_at')->nullable();
$table->decimal('diagram_x', 12, 2)->nullable();
$table->decimal('diagram_y', 12, 2)->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['tenant_id', 'code']);
$table->index(['tenant_id', 'device_type_id', 'status']);
$table->index(['tenant_id', 'latitude', 'longitude']);
});
Schema::create('topology_ports', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('node_id')->constrained('topology_nodes')->cascadeOnDelete();
$table->string('name');
$table->unsignedInteger('port_number');
$table->string('port_type', 50)->default('fiber');
$table->string('direction', 20)->default('bidirectional');
$table->unsignedInteger('capacity')->default(1);
$table->string('status', 30)->default('available');
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(['node_id', 'port_number']);
$table->index(['tenant_id', 'status']);
});
Schema::create('topology_links', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('source_node_id')->constrained('topology_nodes')->restrictOnDelete();
$table->foreignId('source_port_id')->nullable()->constrained('topology_ports')->restrictOnDelete();
$table->foreignId('target_node_id')->constrained('topology_nodes')->restrictOnDelete();
$table->foreignId('target_port_id')->nullable()->constrained('topology_ports')->restrictOnDelete();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
$table->string('code');
$table->string('name');
$table->string('link_type', 30)->default('fiber');
$table->string('fiber_type', 50)->nullable();
$table->unsignedInteger('core_count')->nullable();
$table->decimal('cable_length', 12, 2)->nullable();
$table->decimal('measured_length', 12, 2)->nullable();
$table->json('path_geojson')->nullable();
$table->string('status', 30)->default('active');
$table->date('installed_at')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['tenant_id', 'code']);
$table->unique(['source_node_id', 'target_node_id', 'source_port_id', 'target_port_id'], 'topology_links_endpoints_unique');
$table->index(['tenant_id', 'status', 'link_type']);
});
Schema::create('topology_connection_rules', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained()->cascadeOnDelete();
$table->foreignId('source_device_type_id')->constrained('topology_device_types')->cascadeOnDelete();
$table->foreignId('target_device_type_id')->constrained('topology_device_types')->cascadeOnDelete();
$table->boolean('allowed')->default(true);
$table->unsignedInteger('max_connections')->nullable();
$table->timestamps();
$table->unique(['tenant_id', 'source_device_type_id', 'target_device_type_id'], 'topology_rules_unique');
});
Schema::create('topology_splitters', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('node_id')->constrained('topology_nodes')->cascadeOnDelete();
$table->foreignId('input_port_id')->nullable()->constrained('topology_ports')->nullOnDelete();
$table->string('ratio', 20);
$table->unsignedTinyInteger('level')->default(1);
$table->decimal('insertion_loss_db', 6, 2)->nullable();
$table->string('status', 30)->default('active');
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'node_id']);
});
Schema::create('topology_customer_connections', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->foreignId('odp_node_id')->constrained('topology_nodes')->restrictOnDelete();
$table->foreignId('odp_port_id')->nullable()->constrained('topology_ports')->restrictOnDelete();
$table->foreignId('drop_cable_link_id')->nullable()->constrained('topology_links')->nullOnDelete();
$table->timestamp('connected_at')->nullable();
$table->timestamp('disconnected_at')->nullable();
$table->string('status', 30)->default('active');
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['customer_id', 'status']);
$table->index(['tenant_id', 'odp_node_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('topology_customer_connections');
Schema::dropIfExists('topology_splitters');
Schema::dropIfExists('topology_connection_rules');
Schema::dropIfExists('topology_links');
Schema::dropIfExists('topology_ports');
Schema::dropIfExists('topology_nodes');
Schema::dropIfExists('topology_device_types');
}
};
@@ -0,0 +1,260 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('notification_events', function (Blueprint $table) {
$table->id();
$table->string('code')->unique();
$table->string('name');
$table->string('category', 50);
$table->text('description')->nullable();
$table->json('available_channels');
$table->json('variables')->nullable();
$table->boolean('is_system')->default(true);
$table->boolean('enabled')->default(true);
$table->timestamps();
});
Schema::create('notification_channels', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('channel', 30);
$table->string('provider', 50);
$table->string('name');
$table->boolean('enabled')->default(false);
$table->boolean('is_default')->default(false);
$table->text('credentials')->nullable();
$table->json('settings')->nullable();
$table->string('last_health_status', 30)->nullable();
$table->text('last_health_message')->nullable();
$table->timestamp('last_health_check_at')->nullable();
$table->timestamps();
$table->unique(['tenant_id', 'channel', 'name']);
$table->index(['tenant_id', 'channel', 'enabled']);
});
Schema::create('notification_templates', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained()->cascadeOnDelete();
$table->foreignId('notification_event_id')->constrained()->cascadeOnDelete();
$table->foreignId('notification_channel_id')->nullable()->constrained()->nullOnDelete();
$table->string('channel', 30);
$table->string('name');
$table->string('subject')->nullable();
$table->text('body');
$table->string('provider_template_name')->nullable();
$table->string('provider_template_id')->nullable();
$table->string('language', 10)->default('id');
$table->string('approval_status', 30)->default('not_required');
$table->boolean('enabled')->default(true);
$table->unsignedInteger('version')->default(1);
$table->json('variables')->nullable();
$table->timestamps();
$table->unique(['tenant_id', 'notification_event_id', 'channel', 'language', 'version'], 'notification_templates_version_unique');
$table->index(['tenant_id', 'channel', 'enabled']);
});
Schema::create('notification_preferences', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('notification_event_id')->constrained()->cascadeOnDelete();
$table->foreignId('notification_channel_id')->nullable()->constrained()->nullOnDelete();
$table->string('channel', 30);
$table->boolean('enabled')->default(true);
$table->unsignedTinyInteger('priority')->default(1);
$table->unsignedInteger('send_delay_minutes')->default(0);
$table->time('quiet_hours_start')->nullable();
$table->time('quiet_hours_end')->nullable();
$table->timestamps();
$table->unique(['tenant_id', 'notification_event_id', 'channel']);
});
Schema::create('customer_contacts', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->string('type', 30);
$table->string('value');
$table->string('label')->nullable();
$table->boolean('is_primary')->default(false);
$table->boolean('is_verified')->default(false);
$table->boolean('can_receive_transactional')->default(true);
$table->boolean('can_receive_broadcast')->default(false);
$table->timestamp('verified_at')->nullable();
$table->timestamp('opted_out_at')->nullable();
$table->timestamps();
$table->unique(['customer_id', 'type', 'value']);
$table->index(['tenant_id', 'type', 'is_verified']);
});
Schema::create('notification_broadcasts', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('notification_template_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('notification_channel_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete();
$table->string('name');
$table->string('channel', 30);
$table->string('subject')->nullable();
$table->text('body')->nullable();
$table->string('status', 30)->default('draft');
$table->json('filter_snapshot');
$table->timestamp('scheduled_at')->nullable();
$table->unsignedInteger('estimated_recipients')->default(0);
$table->unsignedInteger('processed_recipients')->default(0);
$table->unsignedInteger('success_count')->default(0);
$table->unsignedInteger('failed_count')->default(0);
$table->timestamps();
$table->index(['tenant_id', 'status', 'scheduled_at']);
});
Schema::create('notification_messages', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('notification_event_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('notification_template_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('notification_broadcast_id')->nullable()->constrained()->cascadeOnDelete();
$table->nullableMorphs('recipient');
$table->nullableMorphs('subjectable');
$table->string('subject')->nullable();
$table->json('payload');
$table->json('context')->nullable();
$table->unsignedTinyInteger('priority')->default(5);
$table->timestamp('scheduled_at')->nullable();
$table->string('status', 30)->default('pending');
$table->timestamps();
$table->index(['tenant_id', 'status', 'scheduled_at']);
});
Schema::create('notification_deliveries', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('notification_message_id')->constrained()->cascadeOnDelete();
$table->foreignId('notification_channel_id')->nullable()->constrained()->nullOnDelete();
$table->string('channel', 30);
$table->string('destination')->nullable();
$table->string('rendered_subject')->nullable();
$table->text('rendered_body');
$table->string('provider_message_id')->nullable()->index();
$table->string('status', 30)->default('pending');
$table->unsignedTinyInteger('attempt_count')->default(0);
$table->timestamp('queued_at')->nullable();
$table->timestamp('sent_at')->nullable();
$table->timestamp('delivered_at')->nullable();
$table->timestamp('read_at')->nullable();
$table->timestamp('failed_at')->nullable();
$table->timestamp('next_retry_at')->nullable();
$table->string('error_code')->nullable();
$table->text('error_message')->nullable();
$table->json('provider_response')->nullable();
$table->timestamps();
$table->index(['channel', 'status', 'next_retry_at']);
});
Schema::create('notification_broadcast_recipients', function (Blueprint $table) {
$table->id();
$table->foreignId('notification_broadcast_id')->constrained()->cascadeOnDelete();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->foreignId('notification_message_id')->nullable()->constrained()->nullOnDelete();
$table->string('destination')->nullable();
$table->string('status', 30)->default('pending');
$table->text('reason')->nullable();
$table->timestamps();
$table->unique(['notification_broadcast_id', 'customer_id']);
});
Schema::create('user_notifications', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('notification_message_id')->nullable()->constrained()->nullOnDelete();
$table->string('title');
$table->text('body');
$table->string('action_url')->nullable();
$table->string('icon')->nullable();
$table->timestamp('read_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'read_at', 'created_at']);
});
Schema::create('notification_event_occurrences', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('notification_event_id')->constrained()->cascadeOnDelete();
$table->morphs('subjectable');
$table->string('occurrence_key');
$table->timestamp('processed_at')->nullable();
$table->timestamps();
$table->unique(['notification_event_id', 'subjectable_type', 'subjectable_id', 'occurrence_key'], 'notification_occurrences_unique');
});
Schema::create('notification_webhook_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('notification_channel_id')->nullable()->constrained()->nullOnDelete();
$table->string('provider', 50);
$table->string('event_type')->nullable();
$table->string('provider_message_id')->nullable()->index();
$table->boolean('signature_valid')->default(false);
$table->json('payload');
$table->timestamp('processed_at')->nullable();
$table->text('error')->nullable();
$table->timestamps();
});
DB::table('customers')
->select(['id', 'tenant_id', 'email', 'whatsapp_number'])
->orderBy('id')
->chunk(500, function ($customers) {
$rows = [];
foreach ($customers as $customer) {
foreach (['email' => $customer->email, 'whatsapp' => $customer->whatsapp_number] as $type => $value) {
if ($value) {
$rows[] = [
'tenant_id' => $customer->tenant_id,
'customer_id' => $customer->id,
'type' => $type,
'value' => $value,
'label' => 'Kontak utama',
'is_primary' => true,
'is_verified' => false,
'can_receive_transactional' => true,
'can_receive_broadcast' => true,
'created_at' => now(),
'updated_at' => now(),
];
}
}
}
if ($rows) {
DB::table('customer_contacts')->insertOrIgnore($rows);
}
});
}
public function down(): void
{
Schema::dropIfExists('notification_webhook_logs');
Schema::dropIfExists('notification_event_occurrences');
Schema::dropIfExists('user_notifications');
Schema::dropIfExists('notification_broadcast_recipients');
Schema::dropIfExists('notification_deliveries');
Schema::dropIfExists('notification_messages');
Schema::dropIfExists('notification_broadcasts');
Schema::dropIfExists('customer_contacts');
Schema::dropIfExists('notification_preferences');
Schema::dropIfExists('notification_templates');
Schema::dropIfExists('notification_channels');
Schema::dropIfExists('notification_events');
}
};
@@ -0,0 +1,203 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('payment_providers', function (Blueprint $table) {
$table->id();
$table->string('code', 50)->unique();
$table->string('name');
$table->string('scope', 20)->default('both');
$table->json('capabilities')->nullable();
$table->json('credential_schema')->nullable();
$table->boolean('enabled')->default(true);
$table->timestamps();
});
Schema::create('payment_gateway_accounts', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('payment_provider_id')->constrained()->restrictOnDelete();
$table->foreignId('tenant_id')->nullable()->constrained()->cascadeOnDelete();
$table->string('scope', 20);
$table->string('name');
$table->string('environment', 20)->default('sandbox');
$table->text('credentials')->nullable();
$table->text('webhook_secret')->nullable();
$table->json('settings')->nullable();
$table->boolean('enabled')->default(false);
$table->boolean('is_default')->default(false);
$table->string('last_health_status', 30)->nullable();
$table->text('last_health_message')->nullable();
$table->timestamp('last_health_check_at')->nullable();
$table->timestamps();
$table->index(['scope', 'tenant_id', 'enabled']);
});
Schema::create('tenant_payment_settings', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->unique()->constrained()->cascadeOnDelete();
$table->string('payment_mode', 30)->default('disabled');
$table->foreignId('tenant_gateway_account_id')->nullable()->constrained('payment_gateway_accounts')->nullOnDelete();
$table->foreignId('platform_gateway_account_id')->nullable()->constrained('payment_gateway_accounts')->nullOnDelete();
$table->string('fee_bearer', 20)->default('customer');
$table->unsignedInteger('default_expiry_minutes')->default(1440);
$table->json('enabled_methods')->nullable();
$table->timestamps();
});
Schema::create('payment_transactions', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('transaction_number')->unique();
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('invoice_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('payment_gateway_account_id')->nullable()->constrained()->nullOnDelete();
$table->string('purpose', 30);
$table->string('gateway_scope', 20);
$table->string('provider_code', 50);
$table->string('provider_reference')->nullable()->index();
$table->string('method', 50)->nullable();
$table->string('currency', 3)->default('IDR');
$table->decimal('amount', 18, 2);
$table->decimal('provider_fee', 18, 2)->default(0);
$table->decimal('platform_fee', 18, 2)->default(0);
$table->decimal('total_amount', 18, 2);
$table->string('status', 30)->default('created');
$table->string('provider_status', 50)->nullable();
$table->string('idempotency_key')->unique();
$table->text('checkout_url')->nullable();
$table->text('qr_content')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamp('paid_at')->nullable();
$table->timestamp('failed_at')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'status', 'created_at']);
$table->index(['user_id', 'purpose', 'status']);
$table->index(['invoice_id', 'status']);
});
Schema::create('payment_attempts', function (Blueprint $table) {
$table->id();
$table->foreignId('payment_transaction_id')->constrained()->cascadeOnDelete();
$table->unsignedSmallInteger('attempt_number');
$table->string('action', 30);
$table->string('status', 30);
$table->json('request_payload')->nullable();
$table->json('response_payload')->nullable();
$table->string('error_code')->nullable();
$table->text('error_message')->nullable();
$table->unsignedInteger('duration_ms')->nullable();
$table->timestamps();
$table->unique(['payment_transaction_id', 'attempt_number', 'action'], 'payment_attempt_unique');
});
Schema::create('payment_webhook_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('payment_gateway_account_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('payment_transaction_id')->nullable()->constrained()->nullOnDelete();
$table->string('provider_code', 50);
$table->string('provider_event_id')->nullable();
$table->boolean('signature_valid')->default(false);
$table->json('headers')->nullable();
$table->json('payload');
$table->string('status', 30)->default('received');
$table->text('error_message')->nullable();
$table->timestamp('processed_at')->nullable();
$table->timestamps();
$table->index(['provider_code', 'status', 'created_at']);
$table->unique(['payment_gateway_account_id', 'provider_event_id']);
});
Schema::create('wallets', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->nullableMorphs('owner');
$table->string('wallet_type', 30);
$table->string('currency', 3)->default('IDR');
$table->decimal('available_balance', 18, 2)->default(0);
$table->decimal('pending_balance', 18, 2)->default(0);
$table->decimal('locked_balance', 18, 2)->default(0);
$table->string('status', 20)->default('active');
$table->unsignedBigInteger('version')->default(0);
$table->timestamps();
$table->unique(['owner_type', 'owner_id', 'wallet_type', 'currency'], 'wallet_owner_type_unique');
});
Schema::create('wallet_transactions', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('transaction_number')->unique();
$table->string('type', 30);
$table->string('status', 20)->default('pending');
$table->foreignId('payment_transaction_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('initiated_by')->nullable()->constrained('users')->nullOnDelete();
$table->decimal('amount', 18, 2);
$table->decimal('fee', 18, 2)->default(0);
$table->string('currency', 3)->default('IDR');
$table->string('idempotency_key')->unique();
$table->text('description')->nullable();
$table->json('metadata')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamps();
$table->index(['type', 'status', 'created_at']);
});
Schema::create('wallet_ledger_entries', function (Blueprint $table) {
$table->id();
$table->foreignId('wallet_id')->constrained()->restrictOnDelete();
$table->foreignId('wallet_transaction_id')->constrained()->restrictOnDelete();
$table->string('entry_type', 10);
$table->string('balance_bucket', 20)->default('available');
$table->decimal('amount', 18, 2);
$table->decimal('balance_before', 18, 2);
$table->decimal('balance_after', 18, 2);
$table->text('description')->nullable();
$table->timestamps();
$table->unique(['wallet_id', 'wallet_transaction_id', 'entry_type', 'balance_bucket'], 'wallet_ledger_unique');
$table->index(['wallet_id', 'created_at']);
});
Schema::create('wallet_withdrawals', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('wallet_id')->constrained()->restrictOnDelete();
$table->foreignId('wallet_transaction_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('requested_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete();
$table->decimal('amount', 18, 2);
$table->decimal('fee', 18, 2)->default(0);
$table->string('bank_code', 30);
$table->string('account_number');
$table->string('account_name');
$table->string('status', 30)->default('pending_approval');
$table->string('provider_reference')->nullable();
$table->text('rejection_reason')->nullable();
$table->timestamp('processed_at')->nullable();
$table->timestamps();
$table->index(['wallet_id', 'status', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('wallet_withdrawals');
Schema::dropIfExists('wallet_ledger_entries');
Schema::dropIfExists('wallet_transactions');
Schema::dropIfExists('wallets');
Schema::dropIfExists('payment_webhook_logs');
Schema::dropIfExists('payment_attempts');
Schema::dropIfExists('payment_transactions');
Schema::dropIfExists('tenant_payment_settings');
Schema::dropIfExists('payment_gateway_accounts');
Schema::dropIfExists('payment_providers');
}
};