fix financial and certificates
This commit is contained in:
+87
@@ -0,0 +1,87 @@
|
||||
<?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
|
||||
{
|
||||
foreach (['students', 'enrollments', 'student_promotion_records'] as $table) {
|
||||
if (!Schema::hasTable($table)) {
|
||||
continue;
|
||||
}
|
||||
Schema::table($table, function (Blueprint $t) use ($table) {
|
||||
if (!Schema::hasColumn($table, 'lifecycle_mode')) {
|
||||
$t->string('lifecycle_mode', 20)->default('legacy');
|
||||
}
|
||||
if (!Schema::hasColumn($table, 'lifecycle_policy_version')) {
|
||||
$t->string('lifecycle_policy_version', 30)->default('legacy_v1');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('section_placement_batches')) {
|
||||
Schema::create('section_placement_batches', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('from_school_year', 25);
|
||||
$table->string('to_school_year', 25);
|
||||
$table->unsignedInteger('from_grade_level_id')->nullable();
|
||||
$table->unsignedInteger('to_grade_level_id')->nullable();
|
||||
$table->unsignedInteger('section_capacity_used');
|
||||
$table->unsignedInteger('total_students')->default(0);
|
||||
$table->unsignedInteger('section_count')->default(0);
|
||||
$table->string('algorithm', 100)->default('score_band_balanced_v1');
|
||||
$table->json('configuration_snapshot_json')->nullable();
|
||||
$table->json('preview_snapshot_json')->nullable();
|
||||
$table->unsignedInteger('created_by')->nullable();
|
||||
$table->unsignedInteger('finalized_by')->nullable();
|
||||
$table->dateTime('finalized_at')->nullable();
|
||||
$table->string('status', 30)->default('draft');
|
||||
$table->timestamps();
|
||||
$table->index(['from_school_year', 'to_school_year', 'status'], 'placement_batches_year_status_idx');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('section_placement_batch_students')) {
|
||||
Schema::create('section_placement_batch_students', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('batch_id');
|
||||
$table->unsignedInteger('student_id');
|
||||
$table->string('student_type', 30);
|
||||
$table->unsignedInteger('source_decision_id')->nullable();
|
||||
$table->unsignedInteger('source_enrollment_id')->nullable();
|
||||
$table->decimal('final_score', 6, 2)->nullable();
|
||||
$table->decimal('placement_score', 6, 2)->nullable();
|
||||
$table->string('score_band', 1);
|
||||
$table->string('placement_band_reason', 60);
|
||||
$table->unsignedInteger('planned_section_index')->nullable();
|
||||
$table->unsignedInteger('assigned_section_id')->nullable();
|
||||
$table->unsignedInteger('assignment_order')->default(0);
|
||||
$table->boolean('was_override')->default(false);
|
||||
$table->text('override_reason')->nullable();
|
||||
$table->dateTime('created_at')->nullable();
|
||||
$table->unique(['batch_id', 'student_id'], 'placement_batch_student_unique');
|
||||
$table->index(['student_id', 'assigned_section_id'], 'placement_batch_student_assignment_idx');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('configuration')) {
|
||||
$exists = DB::table('configuration')->where('config_key', 'promotion.section_capacity')->exists();
|
||||
if (!$exists) {
|
||||
DB::table('configuration')->insert([
|
||||
'config_key' => 'promotion.section_capacity',
|
||||
'config_value' => '20',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('section_placement_batch_students');
|
||||
Schema::dropIfExists('section_placement_batches');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
<?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
|
||||
{
|
||||
foreach (['homework', 'quiz', 'project', 'participation', 'midterm_exam', 'final_exam'] as $table) {
|
||||
if (!Schema::hasTable($table)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Schema::table($table, function (Blueprint $tableBlueprint) use ($table) {
|
||||
if (!Schema::hasColumn($table, 'max_points')) {
|
||||
$tableBlueprint->decimal('max_points', 8, 2)->default(100)->after('score');
|
||||
}
|
||||
if (!Schema::hasColumn($table, 'status')) {
|
||||
$tableBlueprint->string('status', 30)->nullable()->after('max_points');
|
||||
}
|
||||
if (!Schema::hasColumn($table, 'excused_reason')) {
|
||||
$tableBlueprint->text('excused_reason')->nullable()->after('status');
|
||||
}
|
||||
if (!Schema::hasColumn($table, 'locked_at')) {
|
||||
$tableBlueprint->timestamp('locked_at')->nullable()->after('excused_reason');
|
||||
}
|
||||
if (!Schema::hasColumn($table, 'locked_by')) {
|
||||
$tableBlueprint->unsignedBigInteger('locked_by')->nullable()->after('locked_at');
|
||||
}
|
||||
});
|
||||
|
||||
DB::table($table)
|
||||
->whereNull('status')
|
||||
->update([
|
||||
'status' => DB::raw("CASE WHEN score IS NULL THEN 'pending' ELSE 'scored' END"),
|
||||
]);
|
||||
}
|
||||
|
||||
if (Schema::hasTable('semester_scores')) {
|
||||
Schema::table('semester_scores', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('semester_scores', 'calculation_mode')) {
|
||||
$table->string('calculation_mode', 30)->default('legacy')->after('semester_score');
|
||||
}
|
||||
if (!Schema::hasColumn('semester_scores', 'calculation_policy_version')) {
|
||||
$table->string('calculation_policy_version', 50)->default('legacy_v1')->after('calculation_mode');
|
||||
}
|
||||
if (!Schema::hasColumn('semester_scores', 'snapshot_id')) {
|
||||
$table->unsignedBigInteger('snapshot_id')->nullable()->after('calculation_policy_version');
|
||||
}
|
||||
});
|
||||
|
||||
DB::table('semester_scores')->whereNull('calculation_mode')->update(['calculation_mode' => 'legacy']);
|
||||
DB::table('semester_scores')->whereNull('calculation_policy_version')->update(['calculation_policy_version' => 'legacy_v1']);
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('semester_score_snapshots')) {
|
||||
Schema::create('semester_score_snapshots', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('student_id');
|
||||
$table->unsignedBigInteger('school_id')->nullable();
|
||||
$table->unsignedBigInteger('class_section_id');
|
||||
$table->string('semester', 50);
|
||||
$table->string('school_year', 50);
|
||||
$table->unsignedBigInteger('grading_profile_id')->nullable();
|
||||
$table->string('grading_profile_version', 50)->nullable();
|
||||
$table->string('calculation_mode', 30)->default('strong');
|
||||
$table->string('calculation_policy_version', 50)->default('strong_v1');
|
||||
$table->json('input_json')->nullable();
|
||||
$table->json('calculation_json')->nullable();
|
||||
$table->decimal('semester_score', 8, 2)->nullable();
|
||||
$table->unsignedBigInteger('calculated_by')->nullable();
|
||||
$table->timestamp('calculated_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['student_id', 'class_section_id', 'semester', 'school_year'], 'semester_snapshot_student_term_idx');
|
||||
$table->index(['calculation_mode', 'calculation_policy_version'], 'semester_snapshot_policy_idx');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('semester_score_snapshots');
|
||||
|
||||
if (Schema::hasTable('semester_scores')) {
|
||||
Schema::table('semester_scores', function (Blueprint $table) {
|
||||
foreach (['snapshot_id', 'calculation_policy_version', 'calculation_mode'] as $column) {
|
||||
if (Schema::hasColumn('semester_scores', $column)) {
|
||||
$table->dropColumn($column);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
foreach (['homework', 'quiz', 'project', 'participation', 'midterm_exam', 'final_exam'] as $tableName) {
|
||||
if (!Schema::hasTable($tableName)) {
|
||||
continue;
|
||||
}
|
||||
Schema::table($tableName, function (Blueprint $table) use ($tableName) {
|
||||
foreach (['locked_by', 'locked_at', 'excused_reason', 'status', 'max_points'] as $column) {
|
||||
if (Schema::hasColumn($tableName, $column)) {
|
||||
$table->dropColumn($column);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
<?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('finance_follow_up_notes')) {
|
||||
Schema::create('finance_follow_up_notes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('parent_id')->index();
|
||||
$table->unsignedBigInteger('invoice_id')->nullable()->index();
|
||||
$table->string('school_year')->nullable()->index();
|
||||
$table->string('status')->default('not_contacted')->index();
|
||||
$table->string('contact_method')->nullable();
|
||||
$table->date('promise_to_pay_date')->nullable();
|
||||
$table->decimal('promise_to_pay_amount', 12, 2)->nullable();
|
||||
$table->date('next_follow_up_date')->nullable()->index();
|
||||
$table->unsignedTinyInteger('escalation_level')->default(0);
|
||||
$table->text('note')->nullable();
|
||||
$table->unsignedBigInteger('created_by')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('finance_balance_carryforwards')) {
|
||||
Schema::create('finance_balance_carryforwards', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('parent_id')->index();
|
||||
$table->unsignedBigInteger('student_id')->nullable()->index();
|
||||
$table->string('from_school_year')->index();
|
||||
$table->string('to_school_year')->index();
|
||||
$table->json('source_invoice_ids_json')->nullable();
|
||||
$table->decimal('source_balance_amount', 12, 2)->default(0);
|
||||
$table->decimal('carryforward_amount', 12, 2)->default(0);
|
||||
$table->decimal('waived_amount', 12, 2)->default(0);
|
||||
$table->decimal('adjusted_amount', 12, 2)->default(0);
|
||||
$table->decimal('remaining_amount', 12, 2)->default(0);
|
||||
$table->string('status')->default('draft')->index();
|
||||
$table->string('reason')->nullable();
|
||||
$table->unsignedBigInteger('created_by')->nullable()->index();
|
||||
$table->unsignedBigInteger('approved_by')->nullable()->index();
|
||||
$table->timestamp('approved_at')->nullable();
|
||||
$table->unsignedBigInteger('posted_invoice_id')->nullable()->index();
|
||||
$table->unsignedBigInteger('posted_invoice_line_item_id')->nullable()->index();
|
||||
$table->json('metadata_json')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['parent_id', 'from_school_year', 'to_school_year'], 'finance_carryforward_unique_parent_years');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('invoice_installment_plans')) {
|
||||
Schema::create('invoice_installment_plans', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('invoice_id')->index();
|
||||
$table->unsignedBigInteger('parent_id')->index();
|
||||
$table->string('school_year')->nullable()->index();
|
||||
$table->string('plan_name')->nullable();
|
||||
$table->decimal('total_amount', 12, 2)->default(0);
|
||||
$table->unsignedInteger('number_of_installments')->default(1);
|
||||
$table->string('status')->default('draft')->index();
|
||||
$table->unsignedBigInteger('created_by')->nullable()->index();
|
||||
$table->unsignedBigInteger('approved_by')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('invoice_installments')) {
|
||||
Schema::create('invoice_installments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('installment_plan_id')->index();
|
||||
$table->unsignedBigInteger('invoice_id')->index();
|
||||
$table->unsignedInteger('sequence');
|
||||
$table->date('due_date')->index();
|
||||
$table->decimal('amount_due', 12, 2)->default(0);
|
||||
$table->decimal('amount_paid', 12, 2)->default(0);
|
||||
$table->decimal('balance', 12, 2)->default(0);
|
||||
$table->string('status')->default('pending')->index();
|
||||
$table->timestamp('paid_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['installment_plan_id', 'sequence'], 'invoice_installments_plan_sequence_unique');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('payment_installment_allocations')) {
|
||||
Schema::create('payment_installment_allocations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('payment_id')->index();
|
||||
$table->unsignedBigInteger('invoice_id')->index();
|
||||
$table->unsignedBigInteger('installment_id')->index();
|
||||
$table->decimal('amount', 12, 2)->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('finance_notification_logs')) {
|
||||
Schema::create('finance_notification_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('parent_id')->nullable()->index();
|
||||
$table->unsignedBigInteger('student_id')->nullable()->index();
|
||||
$table->unsignedBigInteger('invoice_id')->nullable()->index();
|
||||
$table->unsignedBigInteger('payment_id')->nullable()->index();
|
||||
$table->unsignedBigInteger('refund_id')->nullable()->index();
|
||||
$table->unsignedBigInteger('event_charge_id')->nullable()->index();
|
||||
$table->unsignedBigInteger('installment_id')->nullable()->index();
|
||||
$table->string('notification_type')->index();
|
||||
$table->string('channel')->default('mail')->index();
|
||||
$table->string('recipient')->nullable();
|
||||
$table->string('subject')->nullable();
|
||||
$table->string('status')->default('queued')->index();
|
||||
$table->timestamp('sent_at')->nullable();
|
||||
$table->timestamp('failed_at')->nullable();
|
||||
$table->text('error_message')->nullable();
|
||||
$table->unsignedBigInteger('created_by')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('finance_notification_preferences')) {
|
||||
Schema::create('finance_notification_preferences', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('parent_id')->unique();
|
||||
$table->boolean('email_receipts_enabled')->default(true);
|
||||
$table->boolean('email_reminders_enabled')->default(true);
|
||||
$table->boolean('sms_receipts_enabled')->default(false);
|
||||
$table->boolean('sms_reminders_enabled')->default(false);
|
||||
$table->boolean('statement_notifications_enabled')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('finance_receipt_sequences')) {
|
||||
Schema::create('finance_receipt_sequences', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('school_year')->unique();
|
||||
$table->string('prefix')->default('R');
|
||||
$table->unsignedBigInteger('next_number')->default(1);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('payment_carryforward_allocations')) {
|
||||
Schema::create('payment_carryforward_allocations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('payment_id')->index();
|
||||
$table->unsignedBigInteger('carryforward_id')->index();
|
||||
$table->decimal('amount', 12, 2)->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('payment_carryforward_allocations');
|
||||
Schema::dropIfExists('finance_receipt_sequences');
|
||||
Schema::dropIfExists('finance_notification_preferences');
|
||||
Schema::dropIfExists('finance_notification_logs');
|
||||
Schema::dropIfExists('payment_installment_allocations');
|
||||
Schema::dropIfExists('invoice_installments');
|
||||
Schema::dropIfExists('invoice_installment_plans');
|
||||
Schema::dropIfExists('finance_balance_carryforwards');
|
||||
Schema::dropIfExists('finance_follow_up_notes');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('nav_items')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = DB::table('nav_items')
|
||||
->select(['id', 'menu_parent_id', 'label'])
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('menu_parent_id')
|
||||
->orderBy('label')
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->groupBy(fn ($row) => $row->menu_parent_id === null ? 'root' : (string) $row->menu_parent_id);
|
||||
|
||||
foreach ($rows as $siblings) {
|
||||
$ordered = $siblings->values()->all();
|
||||
usort($ordered, function ($a, $b) {
|
||||
$result = strnatcasecmp($this->labelKey((string) ($a->label ?? '')), $this->labelKey((string) ($b->label ?? '')));
|
||||
|
||||
return $result !== 0 ? $result : ((int) $a->id <=> (int) $b->id);
|
||||
});
|
||||
|
||||
foreach ($ordered as $index => $item) {
|
||||
DB::table('nav_items')
|
||||
->where('id', (int) $item->id)
|
||||
->update([
|
||||
'sort_order' => ($index + 1) * 10,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Alphabetical sort order normalization is intentionally not reversible.
|
||||
}
|
||||
|
||||
private function labelKey(string $s): string
|
||||
{
|
||||
$s = trim(preg_replace('/\s+/', ' ', $s));
|
||||
$s = preg_replace('/^[^[:alnum:]]+/u', '', $s);
|
||||
$s = preg_replace('/^(?i)(the|an|a)\s+/', '', $s);
|
||||
|
||||
return mb_strtolower($s, 'UTF-8');
|
||||
}
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?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('certificate_records') || Schema::hasColumn('certificate_records', 'verification_token')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('certificate_records', function (Blueprint $table): void {
|
||||
$table->string('verification_token', 64)->nullable()->after('certificate_number');
|
||||
$table->unique('verification_token');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('certificate_records') || ! Schema::hasColumn('certificate_records', 'verification_token')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('certificate_records', function (Blueprint $table): void {
|
||||
$table->dropUnique(['verification_token']);
|
||||
$table->dropColumn('verification_token');
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user