add school year and security fix
API CI/CD / Validate (composer + pint) (push) Successful in 3m14s
API CI/CD / Test (PHPUnit) (push) Failing after 3m28s
API CI/CD / Build frontend assets (push) Successful in 1m2s
API CI/CD / Security audit (push) Failing after 56s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
API CI/CD / Validate (composer + pint) (push) Successful in 3m14s
API CI/CD / Test (PHPUnit) (push) Failing after 3m28s
API CI/CD / Build frontend assets (push) Successful in 1m2s
API CI/CD / Security audit (push) Failing after 56s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,162 @@
|
||||
<?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('school_years')) {
|
||||
Schema::create('school_years', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('name', 50)->unique();
|
||||
$table->date('start_date');
|
||||
$table->date('end_date');
|
||||
$table->string('status', 20)->default('draft')->index();
|
||||
$table->boolean('is_current')->default(false)->index();
|
||||
$table->timestamp('closed_at')->nullable();
|
||||
$table->unsignedBigInteger('closed_by')->nullable();
|
||||
$table->timestamp('reopened_at')->nullable();
|
||||
$table->unsignedBigInteger('reopened_by')->nullable();
|
||||
$table->timestamp('archived_at')->nullable();
|
||||
$table->unsignedBigInteger('archived_by')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
} else {
|
||||
Schema::table('school_years', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('school_years', 'reopened_at')) {
|
||||
$table->timestamp('reopened_at')->nullable()->after('closed_by');
|
||||
}
|
||||
if (! Schema::hasColumn('school_years', 'reopened_by')) {
|
||||
$table->unsignedBigInteger('reopened_by')->nullable()->after('reopened_at');
|
||||
}
|
||||
if (! Schema::hasColumn('school_years', 'archived_at')) {
|
||||
$table->timestamp('archived_at')->nullable()->after('reopened_by');
|
||||
}
|
||||
if (! Schema::hasColumn('school_years', 'archived_by')) {
|
||||
$table->unsignedBigInteger('archived_by')->nullable()->after('archived_at');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasTable('student_enrollments')) {
|
||||
Schema::create('student_enrollments', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('student_id');
|
||||
$table->unsignedBigInteger('school_year_id');
|
||||
$table->unsignedBigInteger('grade_id');
|
||||
$table->unsignedBigInteger('class_id')->nullable();
|
||||
$table->string('status', 30)->default('enrolled')->index();
|
||||
$table->unsignedBigInteger('promoted_from_enrollment_id')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['student_id', 'school_year_id'], 'uniq_student_year');
|
||||
$table->index('school_year_id', 'idx_enrollment_school_year');
|
||||
$table->index('student_id', 'idx_enrollment_student');
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasTable('parent_accounts')) {
|
||||
Schema::create('parent_accounts', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('parent_id');
|
||||
$table->string('school_year', 50)->nullable();
|
||||
$table->unsignedBigInteger('school_year_id')->nullable();
|
||||
$table->decimal('opening_balance', 12, 2)->default(0);
|
||||
$table->decimal('current_balance', 12, 2)->default(0);
|
||||
$table->timestamps();
|
||||
$table->unique(['parent_id', 'school_year'], 'uniq_parent_school_year_name');
|
||||
$table->index('parent_id', 'idx_parent_account_parent');
|
||||
$table->index('school_year_id', 'idx_parent_account_school_year_id');
|
||||
});
|
||||
} else {
|
||||
Schema::table('parent_accounts', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('parent_accounts', 'school_year_id')) {
|
||||
$table->unsignedBigInteger('school_year_id')->nullable()->after('school_year');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasTable('parent_balance_transfers')) {
|
||||
Schema::create('parent_balance_transfers', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('parent_id');
|
||||
$table->string('from_school_year', 50)->nullable();
|
||||
$table->string('to_school_year', 50)->nullable();
|
||||
$table->unsignedBigInteger('from_school_year_id')->nullable();
|
||||
$table->unsignedBigInteger('to_school_year_id')->nullable();
|
||||
$table->decimal('amount', 12, 2);
|
||||
$table->string('status', 30)->default('transferred');
|
||||
$table->json('source_summary_json')->nullable();
|
||||
$table->json('source_summary')->nullable();
|
||||
$table->unsignedBigInteger('new_invoice_id')->nullable();
|
||||
$table->unsignedBigInteger('old_balance_invoice_id')->nullable();
|
||||
$table->timestamp('reversed_at')->nullable();
|
||||
$table->unsignedBigInteger('reversed_by')->nullable();
|
||||
$table->text('reversal_reason')->nullable();
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['parent_id', 'from_school_year', 'to_school_year'], 'uniq_parent_year_transfer_name');
|
||||
$table->index('parent_id', 'idx_balance_transfer_parent');
|
||||
$table->index('from_school_year_id', 'idx_balance_transfer_from_year_id');
|
||||
$table->index('to_school_year_id', 'idx_balance_transfer_to_year_id');
|
||||
});
|
||||
} else {
|
||||
Schema::table('parent_balance_transfers', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('parent_balance_transfers', 'from_school_year_id')) {
|
||||
$table->unsignedBigInteger('from_school_year_id')->nullable()->after('to_school_year');
|
||||
}
|
||||
if (! Schema::hasColumn('parent_balance_transfers', 'to_school_year_id')) {
|
||||
$table->unsignedBigInteger('to_school_year_id')->nullable()->after('from_school_year_id');
|
||||
}
|
||||
if (! Schema::hasColumn('parent_balance_transfers', 'source_summary')) {
|
||||
$table->json('source_summary')->nullable()->after('source_summary_json');
|
||||
}
|
||||
if (! Schema::hasColumn('parent_balance_transfers', 'old_balance_invoice_id')) {
|
||||
$table->unsignedBigInteger('old_balance_invoice_id')->nullable()->after('new_invoice_id');
|
||||
}
|
||||
if (! Schema::hasColumn('parent_balance_transfers', 'reversed_at')) {
|
||||
$table->timestamp('reversed_at')->nullable();
|
||||
}
|
||||
if (! Schema::hasColumn('parent_balance_transfers', 'reversed_by')) {
|
||||
$table->unsignedBigInteger('reversed_by')->nullable();
|
||||
}
|
||||
if (! Schema::hasColumn('parent_balance_transfers', 'reversal_reason')) {
|
||||
$table->text('reversal_reason')->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('invoices')) {
|
||||
Schema::table('invoices', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('invoices', 'school_year_id')) {
|
||||
$table->unsignedBigInteger('school_year_id')->nullable()->after('school_year');
|
||||
}
|
||||
if (! Schema::hasColumn('invoices', 'parent_id')) {
|
||||
$table->unsignedBigInteger('parent_id')->nullable();
|
||||
}
|
||||
if (! Schema::hasColumn('invoices', 'student_id')) {
|
||||
$table->unsignedBigInteger('student_id')->nullable()->after('parent_id');
|
||||
}
|
||||
if (! Schema::hasColumn('invoices', 'invoice_type')) {
|
||||
$table->string('invoice_type', 50)->nullable()->after('invoice_number');
|
||||
}
|
||||
if (! Schema::hasColumn('invoices', 'balance_transfer_id')) {
|
||||
$table->unsignedBigInteger('balance_transfer_id')->nullable()->after('invoice_type');
|
||||
}
|
||||
if (! Schema::hasColumn('invoices', 'locked_at')) {
|
||||
$table->timestamp('locked_at')->nullable();
|
||||
}
|
||||
if (! Schema::hasColumn('invoices', 'locked_by')) {
|
||||
$table->unsignedBigInteger('locked_by')->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('student_enrollments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/** @var list<string> */
|
||||
private array $permissions = [
|
||||
'school_year.view',
|
||||
'school_year.view_closed',
|
||||
'school_year.create',
|
||||
'school_year.close',
|
||||
'school_year.reopen',
|
||||
'school_year.archive',
|
||||
'school_year.manage',
|
||||
'school_year.select',
|
||||
'student_enrollment.view',
|
||||
'student_enrollment.manage',
|
||||
'student_enrollment.promote',
|
||||
'student_enrollment.transition',
|
||||
'finance.view',
|
||||
'finance.manage',
|
||||
'finance.invoice.manage',
|
||||
'finance.payment.manage',
|
||||
'finance.balance_transfer.view',
|
||||
'finance.balance_transfer.run',
|
||||
'finance.balance_transfer.reverse',
|
||||
'finance.report.view',
|
||||
'reports.view',
|
||||
'reports.export',
|
||||
'audit.view',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('permissions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->permissions as $permission) {
|
||||
DB::table('permissions')->updateOrInsert(
|
||||
['name' => $permission],
|
||||
$this->filterColumns('permissions', [
|
||||
'description' => $this->describe($permission),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$this->grantByRoleNames(['super admin', 'super_admin', 'administrator', 'admin'], $this->permissions);
|
||||
$this->grantByRoleNames(['director', 'principal'], [
|
||||
'school_year.view',
|
||||
'school_year.view_closed',
|
||||
'school_year.select',
|
||||
'school_year.close',
|
||||
'student_enrollment.view',
|
||||
'student_enrollment.promote',
|
||||
'finance.balance_transfer.view',
|
||||
'reports.view',
|
||||
]);
|
||||
$this->grantByRoleNames(['finance admin', 'finance_admin', 'finance'], [
|
||||
'school_year.view',
|
||||
'school_year.select',
|
||||
'finance.view',
|
||||
'finance.manage',
|
||||
'finance.invoice.manage',
|
||||
'finance.payment.manage',
|
||||
'finance.balance_transfer.view',
|
||||
'finance.balance_transfer.run',
|
||||
'finance.balance_transfer.reverse',
|
||||
'finance.report.view',
|
||||
'reports.view',
|
||||
'reports.export',
|
||||
]);
|
||||
$this->grantByRoleNames(['teacher'], [
|
||||
'school_year.view',
|
||||
'school_year.view_closed',
|
||||
'school_year.select',
|
||||
'student_enrollment.view',
|
||||
]);
|
||||
$this->grantByRoleNames(['parent'], [
|
||||
'school_year.view',
|
||||
'school_year.select',
|
||||
'finance.view',
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('permissions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('permissions')->whereIn('name', $this->permissions)->delete();
|
||||
}
|
||||
|
||||
private function grantByRoleNames(array $roleNames, array $permissions): void
|
||||
{
|
||||
if (! Schema::hasTable('roles') || ! Schema::hasTable('role_permissions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$roleIds = DB::table('roles')
|
||||
->whereIn(DB::raw('LOWER(name)'), array_map('strtolower', $roleNames))
|
||||
->pluck('id')
|
||||
->map(fn ($id) => (int) $id)
|
||||
->all();
|
||||
|
||||
if ($roleIds === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$permissionRows = DB::table('permissions')
|
||||
->whereIn('name', $permissions)
|
||||
->pluck('id', 'name');
|
||||
|
||||
foreach ($roleIds as $roleId) {
|
||||
foreach ($permissionRows as $permissionId) {
|
||||
DB::table('role_permissions')->updateOrInsert(
|
||||
['role_id' => $roleId, 'permission_id' => (int) $permissionId],
|
||||
$this->filterColumns('role_permissions', [
|
||||
'can_create' => true,
|
||||
'can_read' => true,
|
||||
'can_update' => true,
|
||||
'can_delete' => true,
|
||||
'can_manage' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function describe(string $permission): string
|
||||
{
|
||||
return 'Allows '.$permission.' for the school-year closure workflow.';
|
||||
}
|
||||
|
||||
private function filterColumns(string $table, array $values): array
|
||||
{
|
||||
return array_filter(
|
||||
$values,
|
||||
static fn (string $column): bool => Schema::hasColumn($table, $column),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user