update project

This commit is contained in:
root
2026-05-30 01:11:35 -04:00
parent 3a0628ecc7
commit 2225f6bc72
9743 changed files with 1122482 additions and 59 deletions
View File
@@ -0,0 +1,55 @@
<?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
{
if (! Schema::hasTable('schools')) {
Schema::create('schools', function (Blueprint $table) {
$table->id();
$table->string('name')->default('Default School');
$table->string('code')->nullable()->unique();
$table->string('domain_profile')->default('standard_school');
$table->string('timezone')->default('UTC');
$table->string('locale')->default('en');
$table->string('currency', 3)->default('USD');
$table->boolean('active')->default(true);
$table->timestamps();
});
}
if (! Schema::hasTable('students')) {
Schema::create('students', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->string('school_id_number')->nullable();
$table->string('student_identifier')->nullable();
$table->string('first_name');
$table->string('middle_name')->nullable();
$table->string('last_name');
$table->string('preferred_name')->nullable();
$table->date('date_of_birth')->nullable();
$table->string('gender')->nullable();
$table->string('primary_language')->nullable();
$table->string('status')->default('active')->index();
$table->string('external_id')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['school_id', 'status']);
});
}
}
public function down(): void
{
Schema::dropIfExists('students');
Schema::dropIfExists('schools');
}
};
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
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('students', function (Blueprint $table): void {
$table->unique(['school_id', 'school_id_number'], 'students_school_identifier_unique');
});
}
public function down(): void
{
Schema::table('students', function (Blueprint $table): void {
$table->dropUnique('students_school_identifier_unique');
});
}
};
@@ -0,0 +1,93 @@
<?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
{
if (! Schema::hasTable('invoices')) {
Schema::create('invoices', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('student_id')->nullable()->index();
$table->unsignedBigInteger('guardian_id')->nullable()->index();
$table->string('currency', 3)->default('USD');
$table->integer('total_amount_minor')->default(0);
$table->integer('paid_amount_minor')->default(0);
$table->integer('balance_amount_minor')->default(0);
$table->string('status')->default('draft')->index();
$table->date('due_date')->nullable();
$table->text('notes')->nullable();
$table->text('void_reason')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
});
}
if (! Schema::hasTable('invoice_items')) {
Schema::create('invoice_items', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('invoice_id')->index();
$table->string('description');
$table->string('type')->default('charge');
$table->integer('amount_minor');
$table->string('currency', 3)->default('USD');
$table->json('metadata')->nullable();
$table->timestamps();
});
}
if (! Schema::hasTable('payments')) {
Schema::create('payments', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('invoice_id')->nullable()->index();
$table->unsignedBigInteger('student_id')->nullable()->index();
$table->unsignedBigInteger('guardian_id')->nullable()->index();
$table->integer('amount_minor');
$table->string('currency', 3)->default('USD');
$table->string('payment_method')->default('external');
$table->date('payment_date')->nullable()->index();
$table->string('reference_number')->nullable()->index();
$table->text('notes')->nullable();
$table->string('status')->default('recorded')->index();
$table->text('reverse_reason')->nullable();
$table->string('idempotency_key')->nullable()->index();
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(['school_id', 'reference_number'], 'payments_school_reference_unique');
});
}
if (! Schema::hasTable('payment_refunds')) {
Schema::create('payment_refunds', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('invoice_id')->nullable()->index();
$table->unsignedBigInteger('payment_id')->index();
$table->integer('amount_minor');
$table->string('currency', 3)->default('USD');
$table->text('reason')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
});
}
Schema::create('finance_audit_logs', function (Blueprint $table): void {
$table->id(); $table->unsignedBigInteger('school_id')->index(); $table->string('academic_year_id')->nullable()->index(); $table->string('term_id')->nullable()->index(); $table->unsignedBigInteger('actor_user_id')->index(); $table->json('actor_role_snapshot')->nullable(); $table->string('action')->index(); $table->string('entity_type')->index(); $table->string('entity_id')->index(); $table->json('before_json')->nullable(); $table->json('after_json')->nullable(); $table->string('reason', 1000)->nullable(); $table->string('request_id')->nullable()->index(); $table->string('ip_address')->nullable(); $table->text('user_agent')->nullable(); $table->string('idempotency_key')->nullable()->index(); $table->timestamps(); $table->index(['entity_type','entity_id']);
});
Schema::create('payment_files', function (Blueprint $table): void {
$table->id(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('payment_id')->index(); $table->string('file_type')->default('receipt'); $table->string('storage_disk'); $table->string('storage_path'); $table->string('original_name')->nullable(); $table->string('mime_type')->nullable(); $table->json('metadata')->nullable(); $table->timestamps(); $table->unique(['payment_id','file_type']);
});
Schema::create('finance_idempotency_keys', function (Blueprint $table): void {
$table->id(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('actor_user_id')->index(); $table->string('operation')->index(); $table->string('idempotency_key'); $table->string('payload_hash'); $table->string('result_entity_type')->nullable(); $table->string('result_entity_id')->nullable(); $table->timestamp('expires_at')->nullable()->index(); $table->timestamps(); $table->unique(['school_id','actor_user_id','operation','idempotency_key'], 'finance_idempotency_unique');
});
}
public function down(): void
{
Schema::dropIfExists('finance_idempotency_keys'); Schema::dropIfExists('payment_files'); Schema::dropIfExists('finance_audit_logs'); Schema::dropIfExists('payment_refunds'); Schema::dropIfExists('payments'); Schema::dropIfExists('invoice_items'); Schema::dropIfExists('invoices');
}
};
@@ -0,0 +1,78 @@
<?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
{
$this->addIndexIfMissing('payments', 'payments_school_id_index', ['school_id']);
$this->addIndexIfMissing('payments', 'payments_invoice_id_index', ['invoice_id']);
$this->addIndexIfMissing('payments', 'payments_student_id_index', ['student_id']);
$this->addIndexIfMissing('payments', 'payments_guardian_id_index', ['guardian_id']);
$this->addIndexIfMissing('payments', 'payments_status_index', ['status']);
$this->addIndexIfMissing('invoices', 'invoices_school_id_index', ['school_id']);
$this->addIndexIfMissing('invoices', 'invoices_student_id_index', ['student_id']);
$this->addIndexIfMissing('invoices', 'invoices_status_index', ['status']);
$this->addIndexIfMissing('finance_audit_logs', 'finance_audit_logs_school_id_index', ['school_id']);
$this->addIndexIfMissing('finance_audit_logs', 'finance_audit_logs_actor_user_id_index', ['actor_user_id']);
$this->addIndexIfMissing('finance_audit_logs', 'finance_audit_logs_entity_index', ['entity_type', 'entity_id']);
$this->addIndexIfMissing('finance_audit_logs', 'finance_audit_logs_created_at_index', ['created_at']);
$this->addIndexIfMissing('payment_files', 'payment_files_school_id_index', ['school_id']);
$this->addIndexIfMissing('payment_files', 'payment_files_payment_id_index', ['payment_id']);
}
public function down(): void
{
// Intentionally conservative. These indexes may exist from base/legacy schema.
}
private function addIndexIfMissing(string $table, string $indexName, array $columns): void
{
if (! Schema::hasTable($table)) {
return;
}
if ($this->indexExists($table, $indexName)) {
return;
}
Schema::table($table, function (Blueprint $blueprint) use ($indexName, $columns) {
$blueprint->index($columns, $indexName);
});
}
private function indexExists(string $table, string $indexName): bool
{
if (DB::getDriverName() === 'sqlite') {
$indexes = DB::select('PRAGMA index_list('.$table.')');
foreach ($indexes as $index) {
if (($index->name ?? null) === $indexName) {
return true;
}
}
return false;
}
$database = DB::getDatabaseName();
$result = DB::selectOne(
'select count(1) as aggregate
from information_schema.statistics
where table_schema = ?
and table_name = ?
and index_name = ?',
[$database, $table, $indexName]
);
return (int) $result->aggregate > 0;
}
};
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
if (! Schema::hasTable('attendance_sessions')) {
Schema::create('attendance_sessions', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('academic_year_id')->nullable()->index();
$table->unsignedBigInteger('term_id')->nullable()->index();
$table->string('name');
$table->string('session_type')->index();
$table->dateTime('starts_at')->index();
$table->dateTime('ends_at')->index();
$table->string('timezone')->default('UTC');
$table->string('status')->default('open')->index();
$table->json('metadata')->nullable();
$table->timestamps();
});
}
if (! Schema::hasTable('attendance_records')) {
Schema::create('attendance_records', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('attendance_session_id')->nullable()->index();
$table->date('attendance_date')->index();
$table->string('subject_type')->index();
$table->unsignedBigInteger('subject_id')->index();
$table->string('status')->index();
$table->string('source')->default('system');
$table->dateTime('first_seen_at')->nullable();
$table->dateTime('last_seen_at')->nullable();
$table->unsignedBigInteger('recorded_by_user_id')->nullable()->index();
$table->text('override_reason')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(['school_id', 'attendance_session_id', 'subject_type', 'subject_id'], 'att_records_school_session_subject_unique');
});
}
if (! Schema::hasTable('attendance_badges')) {
Schema::create('attendance_badges', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->string('badge_value');
$table->string('badge_type')->nullable();
$table->string('subject_type')->index();
$table->unsignedBigInteger('subject_id')->index();
$table->string('display_name')->nullable();
$table->boolean('active')->default(true)->index();
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(['school_id', 'badge_value']);
});
}
}
public function down(): void
{
Schema::dropIfExists('attendance_badges');
Schema::dropIfExists('attendance_records');
Schema::dropIfExists('attendance_sessions');
}
};
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
if (! Schema::hasTable('attendance_scan_logs')) {
Schema::create('attendance_scan_logs', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('attendance_record_id')->nullable()->index();
$table->string('subject_type')->index();
$table->unsignedBigInteger('subject_id')->index();
$table->unsignedBigInteger('badge_id')->nullable()->index();
$table->string('station_id')->nullable()->index();
$table->string('scan_action');
$table->string('scan_source');
$table->dateTime('scanned_at')->index();
$table->string('status_result');
$table->unsignedBigInteger('duplicate_of_id')->nullable()->index();
$table->json('raw_payload_json')->nullable();
$table->timestamps();
});
}
if (! Schema::hasTable('attendance_audit_logs')) {
Schema::create('attendance_audit_logs', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('academic_year_id')->nullable()->index();
$table->unsignedBigInteger('term_id')->nullable()->index();
$table->unsignedBigInteger('actor_user_id')->nullable()->index();
$table->string('actor_type')->default('user');
$table->string('action')->index();
$table->string('subject_type')->index();
$table->unsignedBigInteger('subject_id')->index();
$table->unsignedBigInteger('attendance_session_id')->nullable()->index();
$table->unsignedBigInteger('attendance_record_id')->nullable()->index();
$table->json('before_json')->nullable();
$table->json('after_json')->nullable();
$table->text('reason')->nullable();
$table->string('source')->default('system');
$table->string('station_id')->nullable()->index();
$table->string('request_id')->nullable()->index();
$table->string('idempotency_key')->nullable()->index();
$table->json('metadata_json')->nullable();
$table->timestamps();
});
}
if (! Schema::hasTable('attendance_idempotency_keys')) {
Schema::create('attendance_idempotency_keys', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->string('operation');
$table->string('idempotency_key');
$table->string('payload_hash');
$table->string('fingerprint')->nullable()->index();
$table->json('result_json')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
$table->unique(['school_id', 'operation', 'idempotency_key'], 'attendance_idem_school_operation_key_unique');
});
}
}
public function down(): void
{
Schema::dropIfExists('attendance_idempotency_keys');
Schema::dropIfExists('attendance_audit_logs');
Schema::dropIfExists('attendance_scan_logs');
}
};
@@ -0,0 +1,3 @@
<?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('students',function(Blueprint $table): void { if(!Schema::hasColumn('students','student_identifier')){$table->string('student_identifier')->nullable()->after('school_id');} if(!Schema::hasColumn('students','status')){$table->string('status')->default('active')->index();} if(!Schema::hasColumn('students','profile_metadata')){$table->json('profile_metadata')->nullable();} $table->index(['school_id','status'],'students_school_status_idx'); }); Schema::create('guardians',function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->string('first_name'); $table->string('last_name'); $table->string('email')->nullable()->index(); $table->string('phone')->nullable()->index(); $table->json('metadata')->nullable(); $table->timestamps(); }); Schema::create('student_guardian',function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('student_id')->index(); $table->unsignedBigInteger('guardian_id')->index(); $table->string('relationship_type')->index(); $table->boolean('emergency_contact')->default(false); $table->boolean('pickup_authorized')->default(false); $table->boolean('financial_responsibility')->nullable(); $table->boolean('academic_contact')->nullable(); $table->timestamp('effective_from')->nullable(); $table->timestamp('effective_to')->nullable(); $table->text('notes')->nullable(); $table->timestamps(); $table->unique(['school_id','student_id','guardian_id'],'student_guardian_unique'); }); Schema::create('households',function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->string('name'); $table->string('primary_address')->nullable(); $table->json('metadata')->nullable(); $table->timestamps(); }); Schema::create('household_student',function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('household_id')->index(); $table->unsignedBigInteger('student_id')->index(); $table->timestamps(); $table->unique(['school_id','household_id','student_id'],'household_student_unique'); }); Schema::create('household_guardian',function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('household_id')->index(); $table->unsignedBigInteger('guardian_id')->index(); $table->timestamps(); $table->unique(['school_id','household_id','guardian_id'],'household_guardian_unique'); }); } public function down(): void { Schema::dropIfExists('household_guardian'); Schema::dropIfExists('household_student'); Schema::dropIfExists('households'); Schema::dropIfExists('student_guardian'); Schema::dropIfExists('guardians'); } };
@@ -0,0 +1,50 @@
<?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
{
if (! $this->indexExists('students', 'students_school_identifier_unique')) {
Schema::table('students', function (Blueprint $table) {
$table->unique(['school_id', 'student_identifier'], 'students_school_identifier_unique');
});
}
Schema::create('enrollments',function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('student_id')->index(); $table->unsignedBigInteger('academic_year_id')->index(); $table->unsignedBigInteger('term_id')->nullable()->index(); $table->unsignedBigInteger('grade_level_id')->nullable()->index(); $table->unsignedBigInteger('program_id')->nullable()->index(); $table->unsignedBigInteger('class_group_id')->nullable()->index(); $table->string('status')->index(); $table->timestamp('enrolled_at')->nullable(); $table->timestamp('withdrawn_at')->nullable(); $table->string('reason')->nullable(); $table->json('metadata')->nullable(); $table->timestamps(); $table->index(['school_id','student_id','academic_year_id','term_id'],'enrollment_period_idx'); });
Schema::create('student_assignments',function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('student_id')->index(); $table->string('assignment_type')->index(); $table->string('assignable_type')->index(); $table->unsignedBigInteger('assignable_id')->index(); $table->date('effective_from')->nullable(); $table->date('effective_to')->nullable(); $table->string('status')->index(); $table->json('metadata')->nullable(); $table->timestamps(); $table->index(['school_id','student_id','assignment_type'],'student_assignment_scope_idx'); });
Schema::create('student_audit_logs',function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('actor_user_id')->nullable()->index(); $table->json('actor_role_snapshot')->nullable(); $table->string('action')->index(); $table->string('entity_type')->index(); $table->unsignedBigInteger('entity_id')->index(); $table->json('before_json')->nullable(); $table->json('after_json')->nullable(); $table->text('reason')->nullable(); $table->string('request_id')->nullable(); $table->string('ip_address')->nullable(); $table->text('user_agent')->nullable(); $table->string('idempotency_key')->nullable()->index(); $table->timestamp('created_at')->useCurrent()->index(); $table->index(['entity_type','entity_id'],'student_audit_entity_idx'); }); }
public function down(): void {
Schema::dropIfExists('student_audit_logs');
Schema::dropIfExists('student_assignments');
Schema::dropIfExists('enrollments'); }
private function indexExists(string $table, string $indexName): bool
{
if (DB::getDriverName() === 'sqlite') {
$indexes = DB::select('PRAGMA index_list('.$table.')');
foreach ($indexes as $index) {
if (($index->name ?? null) === $indexName) {
return true;
}
}
return false;
}
$database = DB::getDatabaseName();
$result = DB::selectOne(
'select count(1) as aggregate
from information_schema.statistics
where table_schema = ?
and table_name = ?
and index_name = ?',
[$database, $table, $indexName]
);
return (int) $result->aggregate > 0;
}
};
@@ -0,0 +1,56 @@
<?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
{
if (Schema::hasTable('islamic_sunday_school_student_profiles')) {
return;
}
Schema::create('islamic_sunday_school_student_profiles', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('school_id');
$table->unsignedBigInteger('student_id');
$table->unsignedBigInteger('masjid_family_id')->nullable();
$table->unsignedBigInteger('quran_level_id')->nullable();
$table->unsignedBigInteger('arabic_level_id')->nullable();
$table->unsignedBigInteger('islamic_studies_track_id')->nullable();
$table->unsignedBigInteger('halaqa_id')->nullable();
$table->string('memorization_progress_summary')->nullable();
$table->string('recitation_placement')->nullable();
$table->boolean('volunteer_family_participation')->default(false);
$table->text('sensitive_admin_notes')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(['school_id', 'student_id'], 'issp_school_student_unique');
$table->index('school_id', 'issp_school_idx');
$table->index('student_id', 'issp_student_idx');
$table->index('masjid_family_id', 'issp_masjid_family_idx');
$table->index('quran_level_id', 'issp_quran_level_idx');
$table->index('arabic_level_id', 'issp_arabic_level_idx');
$table->index('islamic_studies_track_id', 'issp_studies_track_idx');
$table->index('halaqa_id', 'issp_halaqa_idx');
$table->index(['school_id', 'halaqa_id'], 'issp_school_halaqa_idx');
$table->index(['school_id', 'quran_level_id'], 'issp_school_quran_idx');
$table->index(['school_id', 'arabic_level_id'], 'issp_school_arabic_idx');
$table->index(['school_id', 'islamic_studies_track_id'], 'issp_school_studies_idx');
});
}
public function down(): void
{
Schema::dropIfExists('islamic_sunday_school_student_profiles');
}
};
@@ -0,0 +1,199 @@
<?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
{
if (! Schema::hasTable('communication_messages')) {
Schema::create('communication_messages', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('school_id');
$table->unsignedBigInteger('academic_year_id')->nullable();
$table->unsignedBigInteger('term_id')->nullable();
$table->unsignedBigInteger('actor_user_id')->nullable();
$table->string('channel');
$table->string('message_category')->default('general');
$table->string('priority')->default('normal');
$table->string('subject')->nullable();
$table->longText('body_snapshot');
$table->unsignedBigInteger('template_id')->nullable();
$table->string('template_version')->nullable();
$table->string('preview_id')->nullable();
$table->string('status')->default('draft');
$table->timestamp('scheduled_at')->nullable();
$table->timestamp('sent_at')->nullable();
$table->string('idempotency_key')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index('school_id', 'comm_msg_school_idx');
$table->index('actor_user_id', 'comm_msg_actor_idx');
$table->index('channel', 'comm_msg_channel_idx');
$table->index('message_category', 'comm_msg_category_idx');
$table->index('status', 'comm_msg_status_idx');
$table->index('created_at', 'comm_msg_created_idx');
$table->index(['school_id', 'status'], 'comm_msg_school_status_idx');
$table->index(['school_id', 'channel'], 'comm_msg_school_channel_idx');
$table->index(['school_id', 'message_category'], 'comm_msg_school_cat_idx');
$table->index(['school_id', 'idempotency_key'], 'comm_msg_idem_idx');
});
}
if (! Schema::hasTable('communication_message_recipients')) {
Schema::create('communication_message_recipients', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('message_id');
$table->unsignedBigInteger('school_id');
$table->string('recipient_type');
$table->unsignedBigInteger('recipient_id');
$table->string('delivery_address_hash')->nullable();
$table->string('delivery_address_masked')->nullable();
$table->string('channel')->nullable();
$table->string('status')->default('pending');
$table->string('excluded_reason')->nullable();
$table->string('provider_message_id')->nullable();
$table->timestamp('sent_at')->nullable();
$table->timestamp('delivered_at')->nullable();
$table->timestamp('failed_at')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index('message_id', 'comm_rcpt_message_idx');
$table->index('school_id', 'comm_rcpt_school_idx');
$table->index(['recipient_type', 'recipient_id'], 'comm_rcpt_recipient_idx');
$table->index('status', 'comm_rcpt_status_idx');
$table->index('provider_message_id', 'comm_rcpt_provider_idx');
$table->index(['school_id', 'status'], 'comm_rcpt_school_status_idx');
$table->index(['message_id', 'status'], 'comm_rcpt_msg_status_idx');
$table->index(['message_id', 'recipient_type', 'recipient_id'], 'comm_rcpt_msg_recipient_idx');
});
}
if (! Schema::hasTable('communication_recipient_previews')) {
Schema::create('communication_recipient_previews', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->unsignedBigInteger('school_id');
$table->unsignedBigInteger('actor_user_id')->nullable();
$table->string('audience_type');
$table->json('audience_filters_json')->nullable();
$table->string('channel');
$table->string('message_category')->default('general');
$table->unsignedInteger('recipient_count')->default(0);
$table->unsignedInteger('deduped_count')->default(0);
$table->unsignedInteger('blocked_count')->default(0);
$table->unsignedInteger('missing_contact_count')->default(0);
$table->json('snapshot_json')->nullable();
$table->string('confirmation_token_hash');
$table->timestamp('expires_at');
$table->timestamps();
$table->index('school_id', 'comm_prev_school_idx');
$table->index('actor_user_id', 'comm_prev_actor_idx');
$table->index('audience_type', 'comm_prev_audience_idx');
$table->index('channel', 'comm_prev_channel_idx');
$table->index('message_category', 'comm_prev_category_idx');
$table->index('expires_at', 'comm_prev_expires_idx');
$table->index(['school_id', 'expires_at'], 'comm_prev_school_exp_idx');
});
}
if (! Schema::hasTable('communication_audit_logs')) {
Schema::create('communication_audit_logs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('school_id');
$table->unsignedBigInteger('academic_year_id')->nullable();
$table->unsignedBigInteger('term_id')->nullable();
$table->unsignedBigInteger('actor_user_id')->nullable();
$table->json('actor_role_snapshot')->nullable();
$table->string('action');
$table->string('entity_type')->nullable();
$table->unsignedBigInteger('entity_id')->nullable();
$table->json('before_json')->nullable();
$table->json('after_json')->nullable();
$table->string('reason')->nullable();
$table->string('request_id')->nullable();
$table->string('ip_address')->nullable();
$table->text('user_agent')->nullable();
$table->string('idempotency_key')->nullable();
$table->timestamps();
$table->index('school_id', 'comm_audit_school_idx');
$table->index('actor_user_id', 'comm_audit_actor_idx');
$table->index('action', 'comm_audit_action_idx');
$table->index(['entity_type', 'entity_id'], 'comm_audit_entity_idx');
$table->index('created_at', 'comm_audit_created_idx');
$table->index(['school_id', 'action'], 'comm_audit_school_action_idx');
});
}
if (! Schema::hasTable('communication_idempotency_keys')) {
Schema::create('communication_idempotency_keys', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('school_id');
$table->unsignedBigInteger('actor_user_id')->nullable();
$table->string('operation');
$table->string('idempotency_key');
$table->string('payload_hash');
$table->string('result_entity_type')->nullable();
$table->unsignedBigInteger('result_entity_id')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->unique(
['school_id', 'operation', 'idempotency_key'],
'comm_idem_school_op_key_unique'
);
$table->index('school_id', 'comm_idem_school_idx');
$table->index('actor_user_id', 'comm_idem_actor_idx');
$table->index('operation', 'comm_idem_operation_idx');
$table->index('expires_at', 'comm_idem_expires_idx');
});
}
}
public function down(): void
{
Schema::dropIfExists('communication_idempotency_keys');
Schema::dropIfExists('communication_audit_logs');
Schema::dropIfExists('communication_recipient_previews');
Schema::dropIfExists('communication_message_recipients');
Schema::dropIfExists('communication_messages');
}
};
@@ -0,0 +1,194 @@
<?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
{
if (! Schema::hasTable('communication_delivery_attempts')) {
Schema::create('communication_delivery_attempts', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('message_recipient_id');
$table->unsignedBigInteger('school_id');
$table->string('channel', 32);
$table->string('provider', 64)->nullable();
$table->string('provider_message_id', 191)->nullable();
$table->string('status', 32)->default('pending');
$table->string('failure_reason', 64)->nullable();
$table->unsignedInteger('attempt_number')->default(1);
$table->json('raw_provider_payload')->nullable();
$table->timestamp('attempted_at')->nullable();
$table->timestamp('delivered_at')->nullable();
$table->timestamp('failed_at')->nullable();
$table->timestamps();
$table->index('message_recipient_id', 'comm_deliv_recipient_idx');
$table->index('school_id', 'comm_deliv_school_idx');
$table->index('channel', 'comm_deliv_channel_idx');
$table->index('provider_message_id', 'comm_deliv_provider_msg_idx');
$table->index('status', 'comm_deliv_status_idx');
$table->index('created_at', 'comm_deliv_created_idx');
$table->index(['school_id', 'status'], 'comm_deliv_school_status_idx');
$table->index(['message_recipient_id', 'status'], 'comm_deliv_rcpt_status_idx');
});
}
if (! Schema::hasTable('communication_preferences')) {
Schema::create('communication_preferences', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('school_id');
$table->string('recipient_type', 32);
$table->unsignedBigInteger('recipient_id');
$table->string('channel', 32);
$table->string('message_category', 32)->default('general');
$table->boolean('opted_in')->default(true);
$table->boolean('quiet_hours_enabled')->default(false);
$table->time('quiet_hours_start')->nullable();
$table->time('quiet_hours_end')->nullable();
$table->string('preferred_language', 16)->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(
['school_id', 'recipient_type', 'recipient_id', 'channel', 'message_category'],
'comm_pref_unique'
);
$table->index('school_id', 'comm_pref_school_idx');
$table->index(['recipient_type', 'recipient_id'], 'comm_pref_recipient_idx');
$table->index('channel', 'comm_pref_channel_idx');
$table->index('message_category', 'comm_pref_category_idx');
$table->index(['school_id', 'channel'], 'comm_pref_school_channel_idx');
$table->index(['school_id', 'message_category'], 'comm_pref_school_cat_idx');
});
}
if (! Schema::hasTable('communication_templates')) {
Schema::create('communication_templates', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('school_id')->nullable();
$table->string('template_key', 128);
$table->string('version', 32)->default('1');
$table->string('status', 32)->default('draft');
$table->string('channel', 32);
$table->string('message_category', 32)->default('general');
$table->string('language', 16)->default('en');
$table->string('subject', 191)->nullable();
$table->longText('body');
$table->json('variables_schema')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(
['school_id', 'template_key', 'version'],
'comm_tpl_school_key_ver_unique'
);
$table->index('school_id', 'comm_tpl_school_idx');
$table->index('template_key', 'comm_tpl_key_idx');
$table->index('status', 'comm_tpl_status_idx');
$table->index('channel', 'comm_tpl_channel_idx');
$table->index('message_category', 'comm_tpl_category_idx');
$table->index('language', 'comm_tpl_language_idx');
});
}
if (! Schema::hasTable('communication_audit_logs')) {
Schema::create('communication_audit_logs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('school_id');
$table->unsignedBigInteger('academic_year_id')->nullable();
$table->unsignedBigInteger('term_id')->nullable();
$table->unsignedBigInteger('actor_user_id')->nullable();
$table->json('actor_role_snapshot')->nullable();
$table->string('action', 96);
$table->string('entity_type', 96)->nullable();
$table->unsignedBigInteger('entity_id')->nullable();
$table->json('before_json')->nullable();
$table->json('after_json')->nullable();
$table->string('reason', 191)->nullable();
$table->string('request_id', 96)->nullable();
$table->string('ip_address', 64)->nullable();
$table->text('user_agent')->nullable();
$table->string('idempotency_key', 191)->nullable();
$table->json('metadata_json')->nullable();
$table->timestamps();
$table->index('school_id', 'comm_audit_school_idx');
$table->index('actor_user_id', 'comm_audit_actor_idx');
$table->index('action', 'comm_audit_action_idx');
$table->index(['entity_type', 'entity_id'], 'comm_audit_entity_idx');
$table->index('created_at', 'comm_audit_created_idx');
$table->index(['school_id', 'action'], 'comm_audit_school_action_idx');
});
}
if (! Schema::hasTable('communication_idempotency_keys')) {
Schema::create('communication_idempotency_keys', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('school_id');
$table->unsignedBigInteger('actor_user_id')->nullable();
$table->string('operation', 96);
$table->string('idempotency_key', 191);
$table->string('payload_hash', 128);
$table->string('result_entity_type', 96)->nullable();
$table->unsignedBigInteger('result_entity_id')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->unique(
['school_id', 'operation', 'idempotency_key'],
'comm_idem_school_op_key_unique'
);
$table->index('school_id', 'comm_idem_school_idx');
$table->index('actor_user_id', 'comm_idem_actor_idx');
$table->index('operation', 'comm_idem_operation_idx');
$table->index('expires_at', 'comm_idem_expires_idx');
});
}
}
public function down(): void
{
Schema::dropIfExists('communication_idempotency_keys');
Schema::dropIfExists('communication_audit_logs');
Schema::dropIfExists('communication_templates');
Schema::dropIfExists('communication_preferences');
Schema::dropIfExists('communication_delivery_attempts');
}
};
@@ -0,0 +1,46 @@
<?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('report_snapshots', function (Blueprint $table): void {
$table->uuid('id')->primary();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('actor_user_id')->index();
$table->string('report_key')->index();
$table->json('filters_json')->nullable();
$table->json('columns_json')->nullable();
$table->json('result_summary_json')->nullable();
$table->string('snapshot_storage_path')->nullable();
$table->boolean('sensitive_data_flag')->default(false);
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
Schema::create('report_audit_logs', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('actor_user_id')->nullable()->index();
$table->string('action');
$table->string('report_key')->index();
$table->json('filters_json')->nullable();
$table->json('columns_json')->nullable();
$table->string('format')->nullable();
$table->unsignedInteger('row_count')->nullable();
$table->boolean('sensitive_data_flag')->default(false);
$table->string('request_id')->nullable();
$table->timestamps();
$table->index(['school_id', 'report_key', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('report_audit_logs');
Schema::dropIfExists('report_snapshots');
}
};
@@ -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::create('scheduled_reports', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('school_id')->index();
$table->unsignedBigInteger('actor_user_id')->index();
$table->string('name')->nullable();
$table->string('report_key')->index();
$table->json('filters_json')->nullable();
$table->json('columns_json')->nullable();
$table->string('format')->default('csv');
$table->string('frequency');
$table->timestamp('next_run_at')->nullable()->index();
$table->string('status')->default('active')->index();
$table->json('recipient_user_ids_json')->nullable();
$table->json('recipient_emails_json')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->unique(['school_id', 'name']);
});
}
public function down(): void
{
Schema::dropIfExists('scheduled_reports');
}
};
@@ -0,0 +1,23 @@
<?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('api_route_inventory_snapshots', function (Blueprint $table): void {
$table->id();
$table->string('snapshot_key')->unique();
$table->json('routes_json');
$table->unsignedBigInteger('created_by_user_id')->nullable()->index();
$table->timestamp('created_at')->useCurrent();
});
}
public function down(): void
{
Schema::dropIfExists('api_route_inventory_snapshots');
}
};