From e362f68d8db8a42b4fb27be362a768e8f4d6347f Mon Sep 17 00:00:00 2001 From: root Date: Fri, 29 May 2026 05:13:00 -0400 Subject: [PATCH] fix migration --- .env.bak | 65 ++++++ .env.bak-db | 62 ++++++ .env.bak2 | 65 ++++++ app/Providers/AppServiceProvider.php | 18 ++ bootstrap/cache/.gitignore | 0 database/database.sqlite | 0 ...create_base_school_tables_for_scaffold.php | 54 +++++ ..._29_000004_add_modular_finance_indexes.php | 67 +++++- ...ent_enrollment_assignment_audit_tables.php | 39 +++- ...islamic_sunday_school_student_profiles.php | 57 ++++- ...00010_create_communication_core_tables.php | 200 +++++++++++++++++- ...on_delivery_template_preference_tables.php | 195 ++++++++++++++++- routes/api.php | 24 +++ routes/web.php | 10 + .../app/generated/api-route-inventory.json | 0 .../generated/architecture-scan-baseline.json | 0 storage/app/generated/dependency-map.json | 0 .../app/generated/release-feature-flags.json | 0 .../validation-summary.json | 0 .../shadow-comparisons/shadow-summary.json | 0 20 files changed, 838 insertions(+), 18 deletions(-) create mode 100644 .env.bak create mode 100644 .env.bak-db create mode 100644 .env.bak2 create mode 100644 app/Providers/AppServiceProvider.php mode change 100644 => 100755 bootstrap/cache/.gitignore create mode 100644 database/database.sqlite create mode 100644 database/migrations/2026_05_29_000001_create_base_school_tables_for_scaffold.php create mode 100644 routes/api.php create mode 100644 routes/web.php mode change 100644 => 100755 storage/app/generated/api-route-inventory.json mode change 100644 => 100755 storage/app/generated/architecture-scan-baseline.json mode change 100644 => 100755 storage/app/generated/dependency-map.json mode change 100644 => 100755 storage/app/generated/release-feature-flags.json mode change 100644 => 100755 storage/app/generated/release-validation/validation-summary.json mode change 100644 => 100755 storage/app/generated/shadow-comparisons/shadow-summary.json diff --git a/.env.bak b/.env.bak new file mode 100644 index 0000000..c0660ea --- /dev/null +++ b/.env.bak @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/.env.bak-db b/.env.bak-db new file mode 100644 index 0000000..6e87ff0 --- /dev/null +++ b/.env.bak-db @@ -0,0 +1,62 @@ +APP_NAME="Laravel School API" +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost:8000 + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=laravel_school_api +DB_USERNAME=root +DB_PASSWORD= + +SESSION_DRIVER=file +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=sync + +CACHE_STORE=file +CACHE_DRIVER=file + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" \ No newline at end of file diff --git a/.env.bak2 b/.env.bak2 new file mode 100644 index 0000000..e6a1894 --- /dev/null +++ b/.env.bak2 @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=laravel_school_api +DB_USERNAME=root +DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=file +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..70a18d2 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,18 @@ +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'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_05_29_000004_add_modular_finance_indexes.php b/database/migrations/2026_05_29_000004_add_modular_finance_indexes.php index 4c9e229..f2f9b60 100644 --- a/database/migrations/2026_05_29_000004_add_modular_finance_indexes.php +++ b/database/migrations/2026_05_29_000004_add_modular_finance_indexes.php @@ -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; + } +}; \ No newline at end of file diff --git a/database/migrations/2026_05_29_000008_create_student_enrollment_assignment_audit_tables.php b/database/migrations/2026_05_29_000008_create_student_enrollment_assignment_audit_tables.php index 7bb88ed..611008c 100644 --- a/database/migrations/2026_05_29_000008_create_student_enrollment_assignment_audit_tables.php +++ b/database/migrations/2026_05_29_000008_create_student_enrollment_assignment_audit_tables.php @@ -1,3 +1,38 @@ 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; +} + }; diff --git a/database/migrations/2026_05_29_000009_create_islamic_sunday_school_student_profiles.php b/database/migrations/2026_05_29_000009_create_islamic_sunday_school_student_profiles.php index 85e245e..058ed6f 100644 --- a/database/migrations/2026_05_29_000009_create_islamic_sunday_school_student_profiles.php +++ b/database/migrations/2026_05_29_000009_create_islamic_sunday_school_student_profiles.php @@ -1,3 +1,56 @@ 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'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_05_29_000010_create_communication_core_tables.php b/database/migrations/2026_05_29_000010_create_communication_core_tables.php index 60eb8b4..1343a80 100644 --- a/database/migrations/2026_05_29_000010_create_communication_core_tables.php +++ b/database/migrations/2026_05_29_000010_create_communication_core_tables.php @@ -1,3 +1,199 @@ 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'); } }; \ No newline at end of file + +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'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_05_29_000011_create_communication_delivery_template_preference_tables.php b/database/migrations/2026_05_29_000011_create_communication_delivery_template_preference_tables.php index 41ccf7d..ed2caeb 100644 --- a/database/migrations/2026_05_29_000011_create_communication_delivery_template_preference_tables.php +++ b/database/migrations/2026_05_29_000011_create_communication_delivery_template_preference_tables.php @@ -1,3 +1,194 @@ 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'); } }; \ No newline at end of file + +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'); + } +}; \ No newline at end of file diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..45d5b93 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,24 @@ +json([ + 'success' => true, + 'message' => 'API is running.', + ]); +}); + +/* +|-------------------------------------------------------------------------- +| Modular scaffold routes +|-------------------------------------------------------------------------- +| Uncomment these as each route file exists and your providers are registered. +*/ + +// require __DIR__.'/finance_phase3.php'; +// require __DIR__.'/attendance_phase4.php'; +// require __DIR__.'/student_lifecycle_phase5.php'; +// require __DIR__.'/communication_phase6.php'; +// require __DIR__.'/reporting_phase7.php'; +// require __DIR__.'/api_phase8.php'; diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..6814db3 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,10 @@ +json([ + 'success' => true, + 'message' => 'Laravel school API is running.', + ]); +}); diff --git a/storage/app/generated/api-route-inventory.json b/storage/app/generated/api-route-inventory.json old mode 100644 new mode 100755 diff --git a/storage/app/generated/architecture-scan-baseline.json b/storage/app/generated/architecture-scan-baseline.json old mode 100644 new mode 100755 diff --git a/storage/app/generated/dependency-map.json b/storage/app/generated/dependency-map.json old mode 100644 new mode 100755 diff --git a/storage/app/generated/release-feature-flags.json b/storage/app/generated/release-feature-flags.json old mode 100644 new mode 100755 diff --git a/storage/app/generated/release-validation/validation-summary.json b/storage/app/generated/release-validation/validation-summary.json old mode 100644 new mode 100755 diff --git a/storage/app/generated/shadow-comparisons/shadow-summary.json b/storage/app/generated/shadow-comparisons/shadow-summary.json old mode 100644 new mode 100755