1
0

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');
}
};
+3
View File
@@ -17,6 +17,9 @@ class DatabaseSeeder extends Seeder
$this->call([
TicketTypeSeeder::class,
MenuListSeeder::class,
TopologySeeder::class,
NotificationSeeder::class,
PaymentProviderSeeder::class,
]);
User::query()->updateOrCreate(
+158
View File
@@ -188,6 +188,28 @@ class MenuListSeeder extends Seeder
);
}
$paymentGateway = MenuList::updateOrCreate(
['slug' => 'payment-gateway'],
[
'parent_id' => null, 'name' => 'Payment Gateway', 'url' => null,
'icon' => 'cil-credit-card', 'component' => null, 'sort_order' => 80,
]
);
foreach ([
['slug' => 'payment-gateway-settings', 'name' => 'Setting', 'url' => '/payment-gateway/settings', 'sort_order' => 1],
['slug' => 'payment-gateway-logs', 'name' => 'Log Pembayaran', 'url' => '/payment-gateway/logs', 'sort_order' => 2],
] as $item) {
MenuList::updateOrCreate(['slug' => $item['slug']], [
'parent_id' => $paymentGateway->id, 'name' => $item['name'], 'url' => $item['url'],
'icon' => 'cil-credit-card', 'component' => null, 'sort_order' => $item['sort_order'],
]);
}
MenuList::updateOrCreate(['slug' => 'wallet'], [
'parent_id' => null, 'name' => 'Dompet', 'url' => '/wallet',
'icon' => 'cil-wallet', 'component' => null, 'sort_order' => 81,
]);
$nas = MenuList::updateOrCreate(
['slug' => 'nas'],
[
@@ -220,5 +242,141 @@ class MenuListSeeder extends Seeder
]
);
}
$customers = MenuList::updateOrCreate(
['slug' => 'customers'],
[
'parent_id' => null,
'name' => 'Customer',
'url' => null,
'icon' => 'cil-people',
'component' => null,
'sort_order' => 6,
'is_active' => true,
]
);
foreach ([
['slug' => 'customers-orders', 'name' => 'List Order', 'url' => '/customers/orders', 'sort_order' => 1],
['slug' => 'customers-active', 'name' => 'Aktif', 'url' => '/customers/active', 'sort_order' => 2],
['slug' => 'customers-inactive', 'name' => 'Tidak Aktif', 'url' => '/customers/inactive', 'sort_order' => 3],
['slug' => 'customers-unmanaged', 'name' => 'Unmanage', 'url' => '/customers/unmanaged', 'sort_order' => 4],
['slug' => 'customers-trash', 'name' => 'Sampah', 'url' => '/customers/trash', 'sort_order' => 5],
] as $item) {
MenuList::updateOrCreate(
['slug' => $item['slug']],
[
'parent_id' => $customers->id,
'name' => $item['name'],
'url' => $item['url'],
'icon' => 'cil-user',
'component' => null,
'sort_order' => $item['sort_order'],
'is_active' => true,
]
);
}
$billing = MenuList::updateOrCreate(
['slug' => 'billing'],
[
'parent_id' => null,
'name' => 'Tagihan',
'url' => null,
'icon' => 'cil-description',
'component' => null,
'sort_order' => 7,
'is_active' => true,
]
);
foreach ([
['slug' => 'billing-profiles', 'name' => 'Profile Tagihan', 'url' => '/billing/profiles', 'sort_order' => 1],
['slug' => 'billing-running', 'name' => 'Tagihan Berjalan', 'url' => '/billing/running', 'sort_order' => 2],
['slug' => 'billing-overdue', 'name' => 'Tunggakan', 'url' => '/billing/overdue', 'sort_order' => 3],
['slug' => 'billing-paid', 'name' => 'Lunas', 'url' => '/billing/paid', 'sort_order' => 4],
] as $item) {
MenuList::updateOrCreate(
['slug' => $item['slug']],
[
'parent_id' => $billing->id,
'name' => $item['name'],
'url' => $item['url'],
'icon' => 'cil-dollar',
'component' => null,
'sort_order' => $item['sort_order'],
'is_active' => true,
]
);
}
$topology = MenuList::updateOrCreate(
['slug' => 'topology'],
[
'parent_id' => null,
'name' => 'Topologi',
'url' => null,
'icon' => 'cil-layers',
'component' => null,
'sort_order' => 8,
'is_active' => true,
]
);
foreach ([
['slug' => 'topology-map', 'name' => 'Peta Jaringan', 'url' => '/topology/map', 'icon' => 'cil-location-pin', 'sort_order' => 1],
['slug' => 'topology-devices', 'name' => 'Perangkat', 'url' => '/topology/devices', 'icon' => 'cil-settings', 'sort_order' => 2],
['slug' => 'topology-links', 'name' => 'Jalur Kabel', 'url' => '/topology/links', 'icon' => 'cil-layers', 'sort_order' => 3],
['slug' => 'topology-network', 'name' => 'Topologi Jaringan', 'url' => '/topology/network', 'icon' => 'cil-grid', 'sort_order' => 4],
['slug' => 'topology-device-types', 'name' => 'Kategori Perangkat', 'url' => '/topology/device-types', 'icon' => 'cil-list', 'sort_order' => 5],
] as $item) {
MenuList::updateOrCreate(
['slug' => $item['slug']],
[
'parent_id' => $topology->id,
'name' => $item['name'],
'url' => $item['url'],
'icon' => $item['icon'],
'component' => null,
'sort_order' => $item['sort_order'],
'is_active' => true,
]
);
}
$notifications = MenuList::updateOrCreate(
['slug' => 'notifications-service'],
[
'parent_id' => null,
'name' => 'Notifikasi',
'url' => null,
'icon' => 'cil-bell',
'component' => null,
'sort_order' => 9,
'is_active' => true,
]
);
foreach ([
['slug' => 'notifications-broadcasts', 'name' => 'Pesan Siaran', 'url' => '/notifications-service/broadcasts', 'icon' => 'cil-speech', 'sort_order' => 1],
['slug' => 'notifications-system', 'name' => 'Notifikasi Aplikasi', 'url' => '/notifications-service/system', 'icon' => 'cil-bell', 'sort_order' => 2],
['slug' => 'notifications-wa-official', 'name' => 'WhatsApp Official', 'url' => '/notifications-service/whatsapp-official', 'icon' => 'cil-comment-square', 'sort_order' => 3],
['slug' => 'notifications-wa-unofficial', 'name' => 'WhatsApp Unofficial', 'url' => '/notifications-service/whatsapp-unofficial', 'icon' => 'cil-comment-square', 'sort_order' => 4],
['slug' => 'notifications-email', 'name' => 'Email', 'url' => '/notifications-service/email', 'icon' => 'cil-envelope-open', 'sort_order' => 5],
['slug' => 'notifications-telegram', 'name' => 'Telegram', 'url' => '/notifications-service/telegram', 'icon' => 'cil-arrow-right', 'sort_order' => 6],
] as $item) {
MenuList::updateOrCreate(
['slug' => $item['slug']],
[
'parent_id' => $notifications->id,
'name' => $item['name'],
'url' => $item['url'],
'icon' => $item['icon'],
'component' => null,
'sort_order' => $item['sort_order'],
'is_active' => true,
]
);
}
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace Database\Seeders;
use App\Models\NotificationEvent;
use App\Models\NotificationTemplate;
use Illuminate\Database\Seeder;
class NotificationSeeder extends Seeder
{
public function run(): void
{
$events = [
'customer.registration' => ['Registrasi Customer', 'customer', 'Halo {{customer.name}}, registrasi Anda di {{tenant.name}} berhasil diterima.'],
'customer.activation' => ['Aktivasi Customer', 'customer', 'Halo {{customer.name}}, layanan Anda telah aktif. ID pelanggan: {{customer.customer_code}}.'],
'billing.invoice_sent' => ['Kirim Tagihan', 'billing', 'Tagihan {{invoice.invoice_number}} sebesar {{invoice.total}} telah diterbitkan. Batas isolir: {{invoice.isolation_date}}.'],
'billing.warning' => ['Peringatan Tagihan', 'billing', 'Pengingat: tagihan {{invoice.invoice_number}} sebesar {{invoice.balance}} belum dibayar.'],
'billing.isolated' => ['Isolir Pelanggan', 'billing', 'Layanan {{customer.customer_code}} memasuki masa isolir karena tagihan belum dibayar.'],
'voucher_agent.registration' => ['Registrasi Agen Voucher', 'voucher', 'Registrasi agen voucher {{agent.name}} berhasil.'],
'voucher_agent.balance_added' => ['Tambah Saldo Agen Voucher', 'voucher', 'Saldo agen {{agent.name}} bertambah {{balance.amount}}. Saldo saat ini {{balance.current}}.'],
];
$channels = ['system', 'whatsapp_official', 'whatsapp_unofficial', 'email', 'telegram'];
foreach ($events as $code => [$name, $category, $body]) {
$event = NotificationEvent::updateOrCreate(
['code' => $code],
[
'name' => $name,
'category' => $category,
'description' => "Pesan otomatis untuk {$name}",
'available_channels' => $channels,
'variables' => $this->variables($body),
'is_system' => true,
'enabled' => true,
],
);
foreach ($channels as $channel) {
NotificationTemplate::updateOrCreate(
[
'tenant_id' => null,
'notification_event_id' => $event->id,
'channel' => $channel,
'language' => 'id',
'version' => 1,
],
[
'name' => "{$name} - {$this->channelName($channel)}",
'subject' => $channel === 'email' || $channel === 'system' ? $name : null,
'body' => $body,
'approval_status' => $channel === 'whatsapp_official' ? 'pending' : 'not_required',
'enabled' => true,
'variables' => $this->variables($body),
],
);
}
}
}
private function variables(string $body): array
{
preg_match_all('/{{\s*([a-zA-Z0-9_.]+)\s*}}/', $body, $matches);
return array_values(array_unique($matches[1]));
}
private function channelName(string $channel): string
{
return match ($channel) {
'system' => 'Aplikasi',
'whatsapp_official' => 'WhatsApp Official',
'whatsapp_unofficial' => 'WhatsApp Unofficial',
'email' => 'Email',
'telegram' => 'Telegram',
};
}
}
@@ -0,0 +1,27 @@
<?php
namespace Database\Seeders;
use App\Models\PaymentProvider;
use Illuminate\Database\Seeder;
class PaymentProviderSeeder extends Seeder
{
public function run(): void
{
foreach ([
['paydisini', 'Paydisini', 'tenant', ['api_key']],
['winpay', 'Winpay', 'tenant', ['merchant_id', 'api_key', 'private_key']],
['doku', 'DOKU', 'both', ['client_id', 'secret_key']],
['flip_business', 'Flip Business', 'tenant', ['token', 'validation_token']],
['duitku', 'Duitku', 'both', ['merchant_code', 'api_key']],
['midtrans', 'Midtrans', 'platform', ['merchant_id', 'client_key', 'server_key']],
] as [$code, $name, $scope, $credentials]) {
PaymentProvider::updateOrCreate(['code' => $code], [
'name' => $name, 'scope' => $scope, 'enabled' => true,
'capabilities' => ['payment' => true, 'inquiry' => true, 'webhook' => true, 'refund' => in_array($code, ['doku', 'midtrans', 'duitku'])],
'credential_schema' => collect($credentials)->map(fn ($key) => ['key' => $key, 'label' => str($key)->replace('_', ' ')->title(), 'secret' => ! str_contains($key, 'merchant_id') && ! str_contains($key, 'client_id')])->values(),
]);
}
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace Database\Seeders;
use App\Models\TopologyConnectionRule;
use App\Models\TopologyDeviceType;
use Illuminate\Database\Seeder;
class TopologySeeder extends Seeder
{
public function run(): void
{
$definitions = [
['code' => 'POP', 'name' => 'Point of Presence', 'category' => 'site', 'color' => '#dc3545'],
['code' => 'OTB', 'name' => 'Optical Termination Box', 'category' => 'termination', 'color' => '#fd7e14'],
['code' => 'JB', 'name' => 'Joint Box', 'category' => 'closure', 'color' => '#6f42c1'],
['code' => 'ODC', 'name' => 'Optical Distribution Cabinet', 'category' => 'distribution', 'color' => '#0d6efd'],
['code' => 'ODP', 'name' => 'Optical Distribution Point', 'category' => 'distribution', 'color' => '#198754'],
['code' => 'SPLITTER', 'name' => 'Splitter / Ratio', 'category' => 'splitter', 'color' => '#20c997', 'can_have_splitters' => true],
];
$types = collect($definitions)->mapWithKeys(function ($definition) {
$type = TopologyDeviceType::updateOrCreate(
['tenant_id' => null, 'code' => $definition['code']],
[
...$definition,
'tenant_id' => null,
'can_have_ports' => true,
'can_have_splitters' => $definition['can_have_splitters'] ?? false,
'is_system' => true,
'icon' => 'cil-location-pin',
'status' => 'active',
],
);
return [$type->code => $type];
});
foreach ([
['POP', 'POP'], ['POP', 'OTB'], ['OTB', 'JB'], ['OTB', 'ODC'],
['OTB', 'SPLITTER'], ['JB', 'ODC'], ['JB', 'ODP'], ['ODC', 'ODP'],
['ODC', 'SPLITTER'], ['SPLITTER', 'ODP'],
] as [$source, $target]) {
TopologyConnectionRule::updateOrCreate(
[
'tenant_id' => null,
'source_device_type_id' => $types[$source]->id,
'target_device_type_id' => $types[$target]->id,
],
['allowed' => true],
);
}
}
}