This commit is contained in:
Saufi
2026-06-16 15:04:58 +08:00
commit 77a87fbdf1
171 changed files with 21225 additions and 0 deletions

1
src/database/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.sqlite*

View File

@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}

View File

@@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->unsignedBigInteger('role_id')->nullable()->index();
$table->unsignedBigInteger('department_id')->nullable()->index();
$table->boolean('is_active')->default(true);
$table->timestamp('last_login_at')->nullable();
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedSmallInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('connection');
$table->string('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
$table->index(['connection', 'queue', 'failed_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View File

@@ -0,0 +1,263 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('description')->nullable();
$table->timestamps();
});
Schema::create('role_permissions', function (Blueprint $table) {
$table->id();
$table->foreignId('role_id')->constrained()->cascadeOnDelete();
$table->string('module');
$table->json('permissions');
$table->timestamps();
});
Schema::create('departments', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('code')->nullable()->unique();
$table->string('description')->nullable();
$table->timestamps();
});
Schema::create('user_profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
$table->string('public_name')->nullable();
$table->string('identification_type')->nullable();
$table->string('identification_number')->nullable();
$table->string('country_code')->default('+60');
$table->string('mobile_no')->nullable();
$table->string('correspondence_address')->nullable();
$table->string('workplace_address')->nullable();
$table->string('occupation')->nullable();
$table->decimal('monthly_income', 12, 2)->nullable();
$table->timestamps();
});
Schema::create('import_batches', function (Blueprint $table) {
$table->id();
$table->string('filename');
$table->string('stored_path')->nullable();
$table->foreignId('uploaded_by')->nullable()->constrained('users')->nullOnDelete();
$table->unsignedInteger('total_processed')->default(0);
$table->unsignedInteger('total_new')->default(0);
$table->unsignedInteger('total_updated')->default(0);
$table->unsignedInteger('total_missing')->default(0);
$table->unsignedInteger('total_failed')->default(0);
$table->json('summary')->nullable();
$table->timestamps();
});
Schema::create('property_accounts', function (Blueprint $table) {
$table->id();
$table->string('no_akaun')->unique();
$table->string('no_akaun_tunggakan')->unique();
$table->string('pemilik');
$table->string('warganegara')->nullable();
$table->string('pelanggan')->nullable();
$table->string('status')->nullable();
$table->string('bandar')->nullable();
$table->string('taman')->nullable();
$table->string('rupacara')->nullable();
$table->string('kadar')->nullable();
$table->string('aktif')->nullable();
$table->decimal('tunggakan', 12, 2)->default(0);
$table->string('jenis_lot')->nullable();
$table->string('no_lot')->nullable();
$table->string('jenis_dhm')->nullable();
$table->string('no_dhm')->nullable();
$table->string('no_syarikat')->nullable();
$table->string('alamat_harta')->nullable();
$table->string('category')->nullable();
$table->string('status_data')->default('Aktif');
$table->timestamp('last_imported_at')->nullable();
$table->foreignId('import_batch_id')->nullable()->constrained()->nullOnDelete();
$table->timestamps();
});
Schema::create('installment_applications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('property_account_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('assigned_staff_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('application_no')->unique();
$table->string('no_akaun');
$table->string('no_akaun_tunggakan');
$table->string('category');
$table->unsignedTinyInteger('installment_months');
$table->decimal('total_arrears', 12, 2)->default(0);
$table->string('status')->default('diproses');
$table->string('schedule_status')->default('draf');
$table->decimal('schedule_total', 12, 2)->default(0);
$table->text('staff_remarks')->nullable();
$table->foreignId('reviewed_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('reviewed_at')->nullable();
$table->timestamp('approved_at')->nullable();
$table->timestamp('rejected_at')->nullable();
$table->text('rejection_reason')->nullable();
$table->string('identification_type')->nullable();
$table->string('identification_number')->nullable();
$table->string('no_syarikat')->nullable();
$table->string('alamat_harta')->nullable();
$table->string('alamat_surat_menyurat')->nullable();
$table->string('country_code')->default('+60');
$table->string('mobile_no')->nullable();
$table->string('email')->nullable();
$table->string('alamat_tempat_bekerja')->nullable();
$table->string('pekerjaan')->nullable();
$table->decimal('pendapatan_sebulan', 12, 2)->nullable();
$table->string('declaration_name');
$table->string('declaration_identification_type');
$table->string('declaration_identification_number');
$table->date('declaration_date');
$table->boolean('declaration_accepted')->default(false);
$table->string('ic_attachment_path')->nullable();
$table->timestamp('printed_at')->nullable();
$table->timestamps();
});
Schema::create('installment_schedules', function (Blueprint $table) {
$table->id();
$table->foreignId('installment_application_id')->constrained()->cascadeOnDelete();
$table->unsignedTinyInteger('installment_no');
$table->date('due_date')->nullable();
$table->decimal('amount', 12, 2)->default(0);
$table->string('payment_status')->default('belum_bayar');
$table->timestamp('paid_at')->nullable();
$table->text('notes')->nullable();
$table->boolean('is_confirmed')->default(false);
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
});
Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->foreignId('installment_application_id')->constrained()->cascadeOnDelete();
$table->foreignId('installment_schedule_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('transaction_no')->unique();
$table->string('receipt_no')->nullable();
$table->string('biller_code')->default('37317');
$table->string('ref_1');
$table->string('ref_2');
$table->decimal('amount', 12, 2);
$table->string('status')->default('pending');
$table->json('payload')->nullable();
$table->timestamp('paid_at')->nullable();
$table->text('remarks')->nullable();
$table->timestamps();
});
Schema::create('notifications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('type')->default('system');
$table->string('title');
$table->text('message');
$table->string('action_url')->nullable();
$table->boolean('is_read')->default(false);
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
Schema::create('email_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('installment_application_id')->nullable()->constrained()->nullOnDelete();
$table->string('recipient_email');
$table->string('subject');
$table->text('body')->nullable();
$table->string('status')->default('queued');
$table->timestamp('sent_at')->nullable();
$table->timestamps();
});
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('installment_application_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('assigned_by')->nullable()->constrained('users')->nullOnDelete();
$table->string('title');
$table->text('description')->nullable();
$table->string('priority')->default('normal');
$table->string('status')->default('belum_selesai');
$table->timestamp('due_at')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
Schema::create('application_histories', function (Blueprint $table) {
$table->id();
$table->foreignId('installment_application_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('action');
$table->string('old_status')->nullable();
$table->string('new_status')->nullable();
$table->text('remarks')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
});
Schema::create('system_settings', function (Blueprint $table) {
$table->id();
$table->string('group')->default('general');
$table->string('key')->unique();
$table->string('label');
$table->text('value')->nullable();
$table->text('description')->nullable();
$table->timestamps();
});
Schema::create('audit_trails', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('installment_application_id')->nullable()->constrained()->nullOnDelete();
$table->string('action');
$table->string('module');
$table->text('description');
$table->json('old_values')->nullable();
$table->json('new_values')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('audit_trails');
Schema::dropIfExists('system_settings');
Schema::dropIfExists('application_histories');
Schema::dropIfExists('tasks');
Schema::dropIfExists('email_logs');
Schema::dropIfExists('notifications');
Schema::dropIfExists('payments');
Schema::dropIfExists('installment_schedules');
Schema::dropIfExists('installment_applications');
Schema::dropIfExists('property_accounts');
Schema::dropIfExists('import_batches');
Schema::dropIfExists('user_profiles');
Schema::dropIfExists('departments');
Schema::dropIfExists('role_permissions');
Schema::dropIfExists('roles');
}
};

View File

@@ -0,0 +1,24 @@
<?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('installment_applications', function (Blueprint $table) {
$table->string('jenis_lot')->nullable()->after('no_syarikat');
$table->string('no_lot')->nullable()->after('jenis_lot');
$table->string('jenis_dhm')->nullable()->after('no_lot');
});
}
public function down(): void
{
Schema::table('installment_applications', function (Blueprint $table) {
$table->dropColumn(['jenis_lot', 'no_lot', 'jenis_dhm']);
});
}
};

View File

@@ -0,0 +1,24 @@
<?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('property_accounts', function (Blueprint $table) {
$table->string('no_bgnn')->nullable()->after('no_akaun_tunggakan');
$table->string('nama_jalan')->nullable()->after('no_bgnn');
$table->decimal('cukai_harta', 12, 2)->default(0)->after('kadar');
});
}
public function down(): void
{
Schema::table('property_accounts', function (Blueprint $table) {
$table->dropColumn(['no_bgnn', 'nama_jalan', 'cukai_harta']);
});
}
};

View File

@@ -0,0 +1,22 @@
<?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('import_batches', function (Blueprint $table) {
$table->string('import_type')->default('arrears')->after('stored_path');
});
}
public function down(): void
{
Schema::table('import_batches', function (Blueprint $table) {
$table->dropColumn('import_type');
});
}
};

View File

@@ -0,0 +1,22 @@
<?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('installment_applications', function (Blueprint $table): void {
$table->string('no_dhm')->nullable()->after('no_lot');
});
}
public function down(): void
{
Schema::table('installment_applications', function (Blueprint $table): void {
$table->dropColumn('no_dhm');
});
}
};

View File

@@ -0,0 +1,22 @@
<?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('installment_applications', function (Blueprint $table): void {
$table->decimal('property_tax_half_amount', 12, 2)->nullable()->after('total_arrears');
});
}
public function down(): void
{
Schema::table('installment_applications', function (Blueprint $table): void {
$table->dropColumn('property_tax_half_amount');
});
}
};

View File

@@ -0,0 +1,529 @@
<?php
namespace Database\Seeders;
use App\Enums\ApplicationStatus;
use App\Enums\UserRole;
use App\Models\ApplicationHistory;
use App\Models\AuditTrail;
use App\Models\Department;
use App\Models\EmailLog;
use App\Models\ImportBatch;
use App\Models\InstallmentApplication;
use App\Models\InstallmentSchedule;
use App\Models\Payment;
use App\Models\PropertyAccount;
use App\Models\Role;
use App\Models\RolePermission;
use App\Models\SystemNotification;
use App\Models\SystemSetting;
use App\Models\Task;
use App\Models\User;
use App\Models\UserProfile;
use Illuminate\Database\Seeder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
$roles = collect([
UserRole::ADMIN->value => Role::create([
'name' => 'Admin',
'slug' => UserRole::ADMIN->value,
'description' => 'Akses penuh pemantauan, laporan dan tetapan sistem.',
]),
UserRole::STAFF->value => Role::create([
'name' => 'Staff',
'slug' => UserRole::STAFF->value,
'description' => 'Semak permohonan, input jadual manual dan cetak borang.',
]),
UserRole::PUBLIC->value => Role::create([
'name' => 'Pengguna Awam',
'slug' => UserRole::PUBLIC->value,
'description' => 'Mohon ansuran, lihat jadual manual dan buat bayaran dummy.',
]),
]);
foreach ([
UserRole::ADMIN->value => [
'Dashboard' => ['view'],
'Pengurusan Pengguna' => ['view', 'create', 'edit', 'reset_password'],
'Laporan' => ['view', 'export'],
'Audit Trail' => ['view'],
],
UserRole::STAFF->value => [
'Dashboard Staff' => ['view'],
'Permohonan' => ['view', 'review', 'print'],
'Kemaskini Bayaran Pukal' => ['view', 'upload'],
'Import Excel' => ['view', 'upload'],
],
UserRole::PUBLIC->value => [
'Dashboard Pengguna Awam' => ['view'],
'Permohonan Saya' => ['view', 'create'],
'Bayaran Dummy JomPAY' => ['view', 'pay'],
],
] as $roleSlug => $permissions) {
foreach ($permissions as $module => $access) {
RolePermission::create([
'role_id' => $roles[$roleSlug]->id,
'module' => $module,
'permissions' => $access,
]);
}
}
$department = Department::create([
'name' => 'Unit Hasil & Tunggakan',
'code' => 'UHT',
'description' => 'Bahagian pengurusan tunggakan cukai harta dan ansuran.',
]);
$admin = User::create([
'name' => 'Admin MyAnsuran',
'email' => 'admin@myansuran.test',
'password' => Hash::make('password'),
'role_id' => $roles[UserRole::ADMIN->value]->id,
'department_id' => $department->id,
'last_login_at' => now()->subHours(2),
]);
$staff = User::create([
'name' => 'Pegawai Semakan',
'email' => 'staff@myansuran.test',
'password' => Hash::make('password'),
'role_id' => $roles[UserRole::STAFF->value]->id,
'department_id' => $department->id,
'last_login_at' => now()->subHour(),
]);
$publicUser = User::create([
'name' => 'Nur Balqis',
'email' => 'user@myansuran.test',
'password' => Hash::make('password'),
'role_id' => $roles[UserRole::PUBLIC->value]->id,
'last_login_at' => now()->subMinutes(20),
]);
UserProfile::create([
'user_id' => $publicUser->id,
'public_name' => 'Nur Balqis Binti Rahman',
'identification_type' => 'MyKad',
'identification_number' => '920101-01-2345',
'country_code' => '+60',
'mobile_no' => '123456789',
'correspondence_address' => 'No. 88, Jalan Setia 3, Taman Setia Indah, Johor Bahru, Johor',
'workplace_address' => 'Pejabat Pengurusan BizHub, Iskandar Puteri, Johor',
'occupation' => 'Eksekutif Operasi',
'monthly_income' => 4200.00,
]);
$this->seedSystemSettings();
if (! app()->environment('testing')) {
return;
}
$importBatch = ImportBatch::create([
'filename' => 'property-accounts-demo.xlsx',
'stored_path' => 'imports/property-accounts-demo.xlsx',
'import_type' => 'arrears',
'uploaded_by' => $staff->id,
'total_processed' => 10,
'total_new' => 10,
'total_updated' => 0,
'total_missing' => 0,
'total_failed' => 0,
'summary' => ['seeded' => true],
]);
$propertyAccounts = collect([
['A1001', 'NBE1001', 'Nur Balqis Binti Rahman', 'Johor Bahru', 'Taman Setia Indah', 'Banglo', '4.5%', 1200, 'Kediaman'],
['A1002', 'NBE1002', 'Syarikat Maju Bina Sdn Bhd', 'Iskandar Puteri', 'Kawasan Perindustrian Gelang Patah', 'Kilang', '6.0%', 4500, 'Industri'],
['A1003', 'NBE1003', 'Kedai Seri Jaya Enterprise', 'Johor Bahru', 'Taman Molek', 'Kedai Pejabat', '5.5%', 7800, 'Perdagangan'],
['A1004', 'NBE1004', 'Puan Salmah Binti Jusoh', 'Skudai', 'Taman Universiti', 'Teres', '4.0%', 1600, 'Kediaman'],
['A1005', 'NBE1005', 'Warisan Teguh Industries', 'Pasir Gudang', 'Kawasan Industri Tanjung Langsat', 'Gudang', '6.8%', 9800, 'Industri'],
['A1006', 'NBE1006', 'Restoran Selera Selatan', 'Johor Bahru', 'Bandar Dato Onn', 'Premis Niaga', '5.2%', 3100, 'Perdagangan'],
['A1007', 'NBE1007', 'Encik Azhar Bin Jaafar', 'Kulai', 'Taman Putri', 'Teres', '4.1%', 1400, 'Kediaman'],
['A1008', 'NBE1008', 'Mutiara Logistic Sdn Bhd', 'Iskandar Puteri', 'Nusajaya Tech Park', 'Lot Industri', '6.4%', 8600, 'Industri'],
['A1009', 'NBE1009', 'Butik Anggun Mawar', 'Johor Bahru', 'Taman Mount Austin', 'Lot Komersial', '5.1%', 2600, 'Perdagangan'],
['A1010', 'NBE1010', 'Puan Hajar Binti Osman', 'Skudai', 'Taman Sri Pulai', 'Apartment', '3.9%', 900, 'Kediaman'],
])->map(function (array $row) use ($importBatch) {
[$noAkaun, $noAkaunTunggakan, $pemilik, $bandar, $taman, $rupacara, $kadar, $tunggakan, $category] = $row;
return PropertyAccount::create([
'no_akaun' => $noAkaun,
'no_akaun_tunggakan' => $noAkaunTunggakan,
'no_bgnn' => 'BGN-'.str($noAkaun)->after('A'),
'nama_jalan' => 'Jalan Demo '.str($noAkaun)->after('A'),
'pemilik' => $pemilik,
'warganegara' => 'Ya',
'pelanggan' => 'Aktif',
'status' => 'Tunggakan',
'bandar' => $bandar,
'taman' => $taman,
'rupacara' => $rupacara,
'kadar' => $kadar,
'cukai_harta' => round($tunggakan * 0.35, 2),
'aktif' => 'Ya',
'tunggakan' => $tunggakan,
'jenis_lot' => 'Komersial / Kediaman',
'no_lot' => 'LOT-'.str($noAkaun)->after('A'),
'jenis_dhm' => 'Hakmilik Kekal',
'no_dhm' => 'DHM-'.str($noAkaun)->after('A'),
'no_syarikat' => str($pemilik)->contains('Sdn Bhd') ? '201901234567' : null,
'alamat_harta' => $taman.', '.$bandar.', Johor',
'category' => $category,
'status_data' => 'Aktif',
'last_imported_at' => now(),
'import_batch_id' => $importBatch->id,
]);
});
$activeApplication = InstallmentApplication::create([
'user_id' => $publicUser->id,
'property_account_id' => $propertyAccounts[0]->id,
'assigned_staff_id' => $staff->id,
'application_no' => 'MYA-202605-0001',
'no_akaun' => $propertyAccounts[0]->no_akaun,
'no_akaun_tunggakan' => $propertyAccounts[0]->no_akaun_tunggakan,
'category' => 'Kediaman',
'installment_months' => 4,
'total_arrears' => 1200.00,
'status' => ApplicationStatus::ACTIVE->value,
'schedule_status' => 'disahkan',
'schedule_total' => 1200.00,
'staff_remarks' => 'Permohonan lengkap. Bayaran pertama dipandu pada kadar minimum 30%.',
'reviewed_by' => $staff->id,
'reviewed_at' => now()->subDays(5),
'approved_at' => now()->subDays(5),
'identification_type' => 'MyKad',
'identification_number' => '920101-01-2345',
'no_lot' => $propertyAccounts[0]->no_lot,
'no_dhm' => $propertyAccounts[0]->no_dhm,
'no_syarikat' => $propertyAccounts[0]->no_syarikat,
'alamat_harta' => $propertyAccounts[0]->no_bgnn.', '.$propertyAccounts[0]->nama_jalan,
'alamat_surat_menyurat' => 'No. 88, Jalan Setia 3, Taman Setia Indah, Johor Bahru, Johor',
'country_code' => '+60',
'mobile_no' => '123456789',
'email' => $publicUser->email,
'alamat_tempat_bekerja' => 'Pejabat Pengurusan BizHub, Iskandar Puteri, Johor',
'pekerjaan' => 'Eksekutif Operasi',
'pendapatan_sebulan' => 4200.00,
'declaration_name' => 'Nur Balqis Binti Rahman',
'declaration_identification_type' => 'MyKad',
'declaration_identification_number' => '920101-01-2345',
'declaration_date' => now()->subDays(6)->toDateString(),
'declaration_accepted' => true,
'printed_at' => now()->subDays(4),
'created_at' => now()->subDays(6),
'updated_at' => now()->subDays(4),
]);
collect([
[1, 360.00, now()->subDays(4)->toDateString(), 'dibayar'],
[2, 280.00, now()->addDays(5)->toDateString(), 'belum_bayar'],
[3, 280.00, now()->addMonth()->toDateString(), 'belum_bayar'],
[4, 280.00, now()->addMonths(2)->toDateString(), 'belum_bayar'],
])->each(function (array $schedule) use ($activeApplication, $staff) {
InstallmentSchedule::create([
'installment_application_id' => $activeApplication->id,
'installment_no' => $schedule[0],
'amount' => $schedule[1],
'due_date' => $schedule[2],
'payment_status' => $schedule[3],
'paid_at' => $schedule[3] === 'dibayar' ? now()->subDays(3) : null,
'is_confirmed' => true,
'created_by' => $staff->id,
]);
});
$processingApplication = InstallmentApplication::create([
'user_id' => $publicUser->id,
'property_account_id' => $propertyAccounts[1]->id,
'assigned_staff_id' => $staff->id,
'application_no' => 'MYA-202605-0002',
'no_akaun' => $propertyAccounts[1]->no_akaun,
'no_akaun_tunggakan' => $propertyAccounts[1]->no_akaun_tunggakan,
'category' => 'Industri',
'installment_months' => 3,
'total_arrears' => 4500.00,
'status' => ApplicationStatus::PROCESSING->value,
'schedule_status' => 'draf',
'schedule_total' => 0,
'identification_type' => 'MyKad',
'identification_number' => '920101-01-2345',
'no_lot' => $propertyAccounts[1]->no_lot,
'no_dhm' => $propertyAccounts[1]->no_dhm,
'no_syarikat' => $propertyAccounts[1]->no_syarikat,
'alamat_harta' => $propertyAccounts[1]->no_bgnn.', '.$propertyAccounts[1]->nama_jalan,
'alamat_surat_menyurat' => 'No. 88, Jalan Setia 3, Taman Setia Indah, Johor Bahru, Johor',
'country_code' => '+60',
'mobile_no' => '123456789',
'email' => $publicUser->email,
'alamat_tempat_bekerja' => 'Pejabat Pengurusan BizHub, Iskandar Puteri, Johor',
'pekerjaan' => 'Eksekutif Operasi',
'pendapatan_sebulan' => 4200.00,
'declaration_name' => 'Nur Balqis Binti Rahman',
'declaration_identification_type' => 'MyKad',
'declaration_identification_number' => '920101-01-2345',
'declaration_date' => now()->subDays(2)->toDateString(),
'declaration_accepted' => true,
'created_at' => now()->subDays(2),
'updated_at' => now()->subDays(2),
]);
$failedApplication = InstallmentApplication::create([
'user_id' => $publicUser->id,
'property_account_id' => $propertyAccounts[2]->id,
'assigned_staff_id' => $staff->id,
'application_no' => 'MYA-202604-0003',
'no_akaun' => $propertyAccounts[2]->no_akaun,
'no_akaun_tunggakan' => $propertyAccounts[2]->no_akaun_tunggakan,
'category' => 'Perdagangan',
'installment_months' => 2,
'total_arrears' => 7800.00,
'status' => ApplicationStatus::FAILED->value,
'schedule_status' => 'draf',
'schedule_total' => 5000.00,
'staff_remarks' => 'Dokumen sokongan tidak lengkap. Sila kemukakan semula salinan IC yang jelas.',
'reviewed_by' => $staff->id,
'reviewed_at' => now()->subMonth(),
'rejected_at' => now()->subMonth(),
'rejection_reason' => 'Dokumen sokongan tidak lengkap.',
'identification_type' => 'MyKad',
'identification_number' => '920101-01-2345',
'no_lot' => $propertyAccounts[2]->no_lot,
'no_dhm' => $propertyAccounts[2]->no_dhm,
'no_syarikat' => $propertyAccounts[2]->no_syarikat,
'alamat_harta' => $propertyAccounts[2]->no_bgnn.', '.$propertyAccounts[2]->nama_jalan,
'alamat_surat_menyurat' => 'No. 88, Jalan Setia 3, Taman Setia Indah, Johor Bahru, Johor',
'country_code' => '+60',
'mobile_no' => '123456789',
'email' => $publicUser->email,
'alamat_tempat_bekerja' => 'Pejabat Pengurusan BizHub, Iskandar Puteri, Johor',
'pekerjaan' => 'Eksekutif Operasi',
'pendapatan_sebulan' => 4200.00,
'declaration_name' => 'Nur Balqis Binti Rahman',
'declaration_identification_type' => 'MyKad',
'declaration_identification_number' => '920101-01-2345',
'declaration_date' => now()->subMonth()->toDateString(),
'declaration_accepted' => true,
'printed_at' => now()->subMonth()->addDay(),
'created_at' => now()->subMonth()->subDays(2),
'updated_at' => now()->subMonth()->addDay(),
]);
$completedApplication = InstallmentApplication::create([
'user_id' => $publicUser->id,
'property_account_id' => $propertyAccounts[3]->id,
'assigned_staff_id' => $staff->id,
'application_no' => 'MYA-202603-0004',
'no_akaun' => $propertyAccounts[3]->no_akaun,
'no_akaun_tunggakan' => $propertyAccounts[3]->no_akaun_tunggakan,
'category' => 'Kediaman',
'installment_months' => 2,
'total_arrears' => 1600.00,
'status' => ApplicationStatus::COMPLETED->value,
'schedule_status' => 'disahkan',
'schedule_total' => 1600.00,
'staff_remarks' => 'Semua bayaran telah dijelaskan.',
'reviewed_by' => $staff->id,
'reviewed_at' => now()->subMonths(2),
'approved_at' => now()->subMonths(2),
'identification_type' => 'MyKad',
'identification_number' => '920101-01-2345',
'no_lot' => $propertyAccounts[3]->no_lot,
'no_dhm' => $propertyAccounts[3]->no_dhm,
'no_syarikat' => $propertyAccounts[3]->no_syarikat,
'alamat_harta' => $propertyAccounts[3]->no_bgnn.', '.$propertyAccounts[3]->nama_jalan,
'alamat_surat_menyurat' => 'No. 88, Jalan Setia 3, Taman Setia Indah, Johor Bahru, Johor',
'country_code' => '+60',
'mobile_no' => '123456789',
'email' => $publicUser->email,
'alamat_tempat_bekerja' => 'Pejabat Pengurusan BizHub, Iskandar Puteri, Johor',
'pekerjaan' => 'Eksekutif Operasi',
'pendapatan_sebulan' => 4200.00,
'declaration_name' => 'Nur Balqis Binti Rahman',
'declaration_identification_type' => 'MyKad',
'declaration_identification_number' => '920101-01-2345',
'declaration_date' => now()->subMonths(2)->toDateString(),
'declaration_accepted' => true,
'printed_at' => now()->subMonths(2)->addDays(2),
'created_at' => now()->subMonths(2)->subDays(3),
'updated_at' => now()->subMonth(),
]);
collect([
[$completedApplication, 1, 800.00, now()->subMonths(2)->addDays(5)->toDateString()],
[$completedApplication, 2, 800.00, now()->subMonth()->toDateString()],
])->each(function (array $schedule) use ($staff) {
InstallmentSchedule::create([
'installment_application_id' => $schedule[0]->id,
'installment_no' => $schedule[1],
'amount' => $schedule[2],
'due_date' => $schedule[3],
'payment_status' => 'dibayar',
'paid_at' => Carbon::parse($schedule[3])->endOfDay(),
'is_confirmed' => true,
'created_by' => $staff->id,
]);
});
Task::insert([
[
'installment_application_id' => $processingApplication->id,
'assigned_to' => $staff->id,
'assigned_by' => $admin->id,
'title' => 'Semak permohonan '.$processingApplication->application_no,
'description' => 'Permohonan baharu menunggu input amaun ansuran manual.',
'priority' => 'tinggi',
'status' => 'belum_selesai',
'due_at' => now()->addDay(),
'created_at' => now(),
'updated_at' => now(),
],
[
'installment_application_id' => $activeApplication->id,
'assigned_to' => $staff->id,
'assigned_by' => $admin->id,
'title' => 'Pantau baki ansuran '.$activeApplication->application_no,
'description' => 'Semak kemasukan bayaran ansuran bulan kedua.',
'priority' => 'normal',
'status' => 'belum_selesai',
'due_at' => now()->addWeek(),
'created_at' => now(),
'updated_at' => now(),
],
]);
$activePaymentSchedule = $activeApplication->schedules()->first();
Payment::create([
'installment_application_id' => $activeApplication->id,
'installment_schedule_id' => $activePaymentSchedule?->id,
'user_id' => $publicUser->id,
'transaction_no' => 'TRX-DEMO-1001',
'receipt_no' => 'RCPT-DEMO-1001',
'ref_1' => $activeApplication->no_akaun,
'ref_2' => $publicUser->email,
'amount' => 360.00,
'status' => 'success',
'payload' => ['channel' => 'JomPAY Dummy'],
'paid_at' => now()->subDays(3),
'remarks' => 'Bayaran pertama berjaya.',
]);
foreach ($completedApplication->schedules as $index => $schedule) {
Payment::create([
'installment_application_id' => $completedApplication->id,
'installment_schedule_id' => $schedule->id,
'user_id' => $publicUser->id,
'transaction_no' => 'TRX-DEMO-200'.($index + 1),
'receipt_no' => 'RCPT-DEMO-200'.($index + 1),
'ref_1' => $completedApplication->no_akaun,
'ref_2' => $publicUser->email,
'amount' => $schedule->amount,
'status' => 'success',
'payload' => ['channel' => 'JomPAY Dummy'],
'paid_at' => $schedule->paid_at,
'remarks' => 'Bayaran ansuran selesai.',
]);
}
foreach ([
[$activeApplication, $publicUser, 'permohonan_dihantar', null, ApplicationStatus::ACTIVE->value, 'Permohonan diterima dan jadual manual disahkan.'],
[$processingApplication, $publicUser, 'permohonan_dihantar', null, ApplicationStatus::PROCESSING->value, 'Permohonan sedang diproses oleh staff.'],
[$failedApplication, $staff, 'permohonan_ditolak', ApplicationStatus::PROCESSING->value, ApplicationStatus::FAILED->value, 'Dokumen sokongan tidak lengkap.'],
[$completedApplication, $staff, 'akaun_ansuran_selesai', ApplicationStatus::ACTIVE->value, ApplicationStatus::COMPLETED->value, 'Semua ansuran telah dibayar.'],
] as $history) {
ApplicationHistory::create([
'installment_application_id' => $history[0]->id,
'user_id' => $history[1]->id,
'action' => $history[2],
'old_status' => $history[3],
'new_status' => $history[4],
'remarks' => $history[5],
'metadata' => ['seeded' => true],
'created_at' => now(),
'updated_at' => now(),
]);
}
foreach ([
['Permohonan diterima', 'Permohonan MYA-202605-0001 telah disahkan dan jadual bayaran dipaparkan.', $activeApplication],
['Permohonan masih diproses', 'Permohonan MYA-202605-0002 sedang disemak oleh staff.', $processingApplication],
['Permohonan ditolak', 'Permohonan MYA-202604-0003 ditolak. Sila semak ulasan pegawai.', $failedApplication],
] as $notification) {
SystemNotification::create([
'user_id' => $publicUser->id,
'type' => 'application',
'title' => $notification[0],
'message' => $notification[1],
'action_url' => '/portal/applications/'.$notification[2]->id,
]);
}
foreach ([
['Permohonan diterima', 'Jadual bayaran manual untuk MYA-202605-0001 telah disahkan.', $activeApplication],
['Permohonan ditolak', 'Permohonan MYA-202604-0003 memerlukan semakan semula dokumen.', $failedApplication],
] as $email) {
EmailLog::create([
'user_id' => $publicUser->id,
'installment_application_id' => $email[2]->id,
'recipient_email' => $publicUser->email,
'subject' => $email[0],
'body' => $email[1],
'status' => 'sent',
'sent_at' => now(),
]);
}
foreach ([
[$admin, 'login', 'Authentication', 'Admin log masuk ke sistem.'],
[$publicUser, 'hantar_permohonan', 'Permohonan Ansuran', 'Pengguna menghantar permohonan baharu.'],
[$staff, 'semak_permohonan', 'Semakan Staff', 'Staff mengesahkan jadual ansuran manual.'],
[$staff, 'upload_excel', 'Import Excel', 'Staff memuat naik Excel data akaun cukai harta.'],
[$publicUser, 'bayaran_dummy', 'Pembayaran', 'Pengguna membuat bayaran dummy melalui JomPAY.'],
] as $audit) {
AuditTrail::create([
'user_id' => $audit[0]->id,
'installment_application_id' => $activeApplication->id,
'action' => $audit[1],
'module' => $audit[2],
'description' => $audit[3],
'old_values' => ['seeded' => true],
'new_values' => ['seeded' => true],
'ip_address' => '127.0.0.1',
'user_agent' => 'Seeder',
'created_at' => now(),
'updated_at' => now(),
]);
}
}
protected function seedSystemSettings(): void
{
foreach ([
['general', 'max_installment_months', 'Jumlah maksimum ansuran', '6', 'Had maksimum tempoh ansuran yang dibenarkan.'],
['general', 'minimum_first_payment_residential', 'Kediaman - Panduan Bayaran Awal', '30', 'Peratus panduan bayaran pertama untuk kediaman.'],
['general', 'minimum_first_payment_industrial', 'Industri - Panduan Bayaran Awal', '50', 'Peratus panduan bayaran pertama untuk industri.'],
['general', 'minimum_first_payment_commercial', 'Perdagangan - Panduan Bayaran Awal', '50', 'Peratus panduan bayaran pertama untuk perdagangan.'],
['department', 'department_name', 'Nama Jabatan', 'Majlis Bandaraya Iskandar Puteri', 'Nama jabatan untuk paparan portal.'],
['payment', 'jompay_biller_code', 'Biller Code JomPAY', '37317', 'Kod pengebil untuk demo pembayaran JomPAY.'],
] as $setting) {
SystemSetting::updateOrCreate(
['key' => $setting[1]],
[
'group' => $setting[0],
'label' => $setting[2],
'value' => $setting[3],
'description' => $setting[4],
],
);
}
}
}