fix migration

This commit is contained in:
root
2026-05-29 05:13:00 -04:00
parent cdeab1796f
commit e362f68d8d
20 changed files with 838 additions and 18 deletions
@@ -0,0 +1,54 @@
<?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('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');
}
};
@@ -2,18 +2,65 @@
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 {
return new class extends Migration
{
public function up(): void
{
foreach (['payments','invoices'] as $tableName) {
if (! Schema::hasTable($tableName)) { continue; }
Schema::table($tableName, function (Blueprint $table) use ($tableName): void {
if (Schema::hasColumn($tableName, 'school_id')) { $table->index('school_id'); }
if (Schema::hasColumn($tableName, 'status')) { $table->index('status'); }
});
}
$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 {}
};
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
{
$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;
}
};
@@ -1,3 +1,38 @@
<?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 { $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'); } };
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
{
$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;
}
};
@@ -1,3 +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 { Schema::create('islamic_sunday_school_student_profiles',function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('student_id')->index(); $table->unsignedBigInteger('family_profile_id')->nullable()->index(); $table->unsignedBigInteger('quran_level_id')->nullable()->index(); $table->unsignedBigInteger('arabic_level_id')->nullable()->index(); $table->unsignedBigInteger('islamic_studies_track_id')->nullable()->index(); $table->unsignedBigInteger('halaqa_group_id')->nullable()->index(); $table->text('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'],'iss_student_profile_unique'); }); } public function down(): void { Schema::dropIfExists('islamic_sunday_school_student_profiles'); } };
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');
}
};
@@ -1,3 +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 { Schema::create('communication_recipient_previews', function(Blueprint $table): void { $table->uuid('id')->primary(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('actor_user_id')->index(); $table->string('audience_type')->index(); $table->json('audience_filters_json')->nullable(); $table->string('channel')->index(); $table->string('message_category')->index(); $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->longText('snapshot_json'); $table->string('confirmation_token_hash'); $table->timestamp('expires_at')->index(); $table->timestamps(); }); Schema::create('communication_messages', function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('actor_user_id')->index(); $table->string('channel')->index(); $table->string('message_category')->index(); $table->string('priority')->default('normal'); $table->string('subject')->nullable(); $table->longText('body_snapshot'); $table->uuid('preview_id')->nullable()->index(); $table->string('status')->index(); $table->timestamp('scheduled_at')->nullable(); $table->timestamp('sent_at')->nullable(); $table->timestamps(); }); Schema::create('communication_message_recipients', function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('message_id')->index(); $table->unsignedBigInteger('school_id')->index(); $table->string('recipient_type'); $table->string('recipient_id'); $table->string('channel')->index(); $table->string('delivery_address_hash')->nullable(); $table->string('delivery_address_masked')->nullable(); $table->string('status')->index(); $table->string('excluded_reason')->nullable(); $table->string('provider_message_id')->nullable()->index(); $table->timestamps(); $table->index(['recipient_type','recipient_id']); }); } public function down(): void { Schema::dropIfExists('communication_message_recipients'); Schema::dropIfExists('communication_messages'); Schema::dropIfExists('communication_recipient_previews'); } };
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');
}
};
@@ -1,3 +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 { Schema::create('communication_delivery_attempts', function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('message_recipient_id')->index(); $table->string('provider_message_id')->nullable()->index(); $table->string('status')->index(); $table->string('failure_reason')->nullable(); $table->unsignedInteger('attempt_number')->default(1); $table->json('raw_provider_payload')->nullable(); $table->timestamps(); }); Schema::create('communication_preferences', function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->string('recipient_type'); $table->string('recipient_id'); $table->string('channel')->index(); $table->string('message_category')->default('general'); $table->boolean('allowed')->default(true); $table->json('metadata_json')->nullable(); $table->timestamps(); $table->index(['recipient_type','recipient_id']); }); Schema::create('communication_templates', function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->nullable()->index(); $table->string('template_key')->index(); $table->unsignedInteger('version')->default(1); $table->string('name'); $table->string('subject')->nullable(); $table->longText('body'); $table->string('language')->default('en'); $table->json('required_variables_json')->nullable(); $table->string('status')->index(); $table->json('metadata_json')->nullable(); $table->timestamps(); $table->unique(['school_id','template_key','version'], 'comm_templates_school_key_version_unique'); }); Schema::create('communication_audit_logs', function(Blueprint $table): void { $table->id(); $table->unsignedBigInteger('school_id')->index(); $table->unsignedBigInteger('actor_user_id')->nullable()->index(); $table->string('action')->index(); $table->string('entity_type'); $table->string('entity_id'); $table->json('before_json')->nullable(); $table->json('after_json')->nullable(); $table->string('reason')->nullable(); $table->string('idempotency_key')->nullable(); $table->json('metadata_json')->nullable(); $table->timestamps(); $table->index(['entity_type','entity_id']); }); } public function down(): void { Schema::dropIfExists('communication_audit_logs'); Schema::dropIfExists('communication_templates'); Schema::dropIfExists('communication_preferences'); Schema::dropIfExists('communication_delivery_attempts'); } };
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', 64);
$table->unsignedBigInteger('recipient_id');
$table->string('channel', 32);
$table->string('message_category', 64)->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', 64)->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');
}
};