commit9
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-30 20:02:30 +02:00
parent 6d14b4e6f0
commit c6373365bf
225 changed files with 15365 additions and 285 deletions
@@ -0,0 +1,20 @@
<?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('tenants', fn (Blueprint $table) => $table->decimal('default_tax_rate', 5, 2)->default('16.00'));
Schema::table('quotations', fn (Blueprint $table) => $table->json('commercial_terms')->nullable());
}
public function down(): void
{
Schema::table('quotations', fn (Blueprint $table) => $table->dropColumn('commercial_terms'));
Schema::table('tenants', fn (Blueprint $table) => $table->dropColumn('default_tax_rate'));
}
};
@@ -0,0 +1,20 @@
<?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('tenants', fn (Blueprint $table) => $table->json('comparison_policy')->nullable());
Schema::table('requisitions', fn (Blueprint $table) => $table->json('comparison_policy')->nullable());
}
public function down(): void
{
Schema::table('requisitions', fn (Blueprint $table) => $table->dropColumn('comparison_policy'));
Schema::table('tenants', fn (Blueprint $table) => $table->dropColumn('comparison_policy'));
}
};
@@ -0,0 +1,89 @@
<?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
{
Schema::create('people_processes', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
$table->uuid('operation_key');
$table->string('creation_hash', 64);
$table->string('kind', 20);
$table->string('title', 200);
$table->text('purpose');
$table->date('closes_on');
$table->json('rules');
$table->string('status', 30)->default('draft');
$table->unsignedInteger('version')->default(1);
$table->foreignId('opened_by')->nullable()->constrained('users')->restrictOnDelete();
$table->foreignId('selected_application_id')->nullable();
$table->json('selection')->nullable();
$table->foreignId('confirmed_by')->nullable()->constrained('users')->restrictOnDelete();
$table->json('contract')->nullable();
$table->timestamps();
$table->unique(['tenant_id', 'operation_key']);
});
Schema::create('people_applications', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('people_process_id')->constrained()->restrictOnDelete();
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
$table->uuid('operation_key');
$table->string('name', 200);
$table->string('reference', 200);
$table->text('summary');
$table->date('received_on');
$table->timestamps();
$table->unique(['people_process_id', 'reference']);
$table->unique(['tenant_id', 'operation_key']);
});
Schema::create('people_reviews', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('people_application_id')->constrained()->restrictOnDelete();
$table->foreignId('reviewed_by')->constrained('users')->restrictOnDelete();
$table->json('answers');
$table->text('reason');
$table->unsignedInteger('score_minor');
$table->timestamps();
$table->unique(['people_application_id', 'reviewed_by']);
});
if (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION sgc_preserve_people_evidence() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
RAISE EXCEPTION 'Preservar candidatura e avaliacao'; END $$;
CREATE TRIGGER preserve_people_applications BEFORE UPDATE OR DELETE ON people_applications FOR EACH ROW EXECUTE FUNCTION sgc_preserve_people_evidence();
CREATE TRIGGER preserve_people_reviews BEFORE UPDATE OR DELETE ON people_reviews FOR EACH ROW EXECUTE FUNCTION sgc_preserve_people_evidence();
CREATE FUNCTION sgc_preserve_people_process() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP = 'DELETE' OR OLD.status = 'contract_draft' THEN RAISE EXCEPTION 'Preservar processo e minuta'; END IF;
IF ROW(NEW.tenant_id, NEW.created_by, NEW.kind, NEW.title, NEW.purpose, NEW.closes_on, NEW.rules::text, NEW.operation_key, NEW.creation_hash)
IS DISTINCT FROM ROW(OLD.tenant_id, OLD.created_by, OLD.kind, OLD.title, OLD.purpose, OLD.closes_on, OLD.rules::text, OLD.operation_key, OLD.creation_hash)
THEN RAISE EXCEPTION 'Preservar regras e ambito do processo'; END IF;
RETURN NEW; END $$;
CREATE TRIGGER preserve_people_process BEFORE UPDATE OR DELETE ON people_processes FOR EACH ROW EXECUTE FUNCTION sgc_preserve_people_process();");
} elseif (DB::getDriverName() === 'sqlite') {
foreach (['people_applications', 'people_reviews'] as $table) {
foreach (['UPDATE', 'DELETE'] as $operation) {
DB::unprepared("CREATE TRIGGER preserve_{$table}_{$operation} BEFORE {$operation} ON {$table} BEGIN SELECT RAISE(ABORT, 'Preservar evidencias'); END;");
}
}
DB::unprepared("CREATE TRIGGER preserve_people_process_delete BEFORE DELETE ON people_processes BEGIN SELECT RAISE(ABORT, 'Preservar processo'); END;");
DB::unprepared("CREATE TRIGGER preserve_people_process_update BEFORE UPDATE ON people_processes WHEN OLD.status = 'contract_draft'
OR NEW.tenant_id IS NOT OLD.tenant_id OR NEW.created_by IS NOT OLD.created_by OR NEW.kind IS NOT OLD.kind OR NEW.title IS NOT OLD.title
OR NEW.purpose IS NOT OLD.purpose OR NEW.closes_on IS NOT OLD.closes_on OR NEW.rules IS NOT OLD.rules
OR NEW.operation_key IS NOT OLD.operation_key OR NEW.creation_hash IS NOT OLD.creation_hash
BEGIN SELECT RAISE(ABORT, 'Preservar regras e processo'); END;");
}
}
public function down(): void
{
throw new LogicException('Preservar processos de contratação e evidências.');
}
};
@@ -0,0 +1,66 @@
<?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
{
Schema::create('people_access_states', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('people_process_id')->unique()->constrained()->restrictOnDelete();
$table->unsignedInteger('version')->default(1);
});
Schema::create('people_process_access', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('people_process_id')->constrained()->restrictOnDelete();
$table->foreignId('user_id')->constrained()->restrictOnDelete();
$table->json('scopes');
$table->timestamp('expires_at')->nullable();
$table->text('reason');
$table->timestamps();
$table->unique(['people_process_id', 'user_id']);
});
// Preserve actual past participation, never grant all administrators access to content.
foreach (DB::table('people_processes')->orderBy('id')->get() as $process) {
DB::table('people_access_states')->insert(['tenant_id' => $process->tenant_id, 'people_process_id' => $process->id, 'version' => 1]);
$people = [$process->created_by => ['view', 'prepare', 'contract', 'manage']];
foreach (DB::table('people_applications')->where('people_process_id', $process->id)->pluck('created_by') as $person) {
$people[$person] = [...($people[$person] ?? []), 'view', 'prepare'];
}
$selection = $process->selection === null ? [] : json_decode($process->selection, true, 32, JSON_THROW_ON_ERROR);
if (isset($selection['proposed_by'])) {
$person = $selection['proposed_by'];
$people[$person] = [...($people[$person] ?? []), 'view', 'prepare'];
}
$contract = $process->contract === null ? [] : json_decode($process->contract, true, 32, JSON_THROW_ON_ERROR);
if (isset($contract['prepared_by'])) {
$person = $contract['prepared_by'];
$people[$person] = [...($people[$person] ?? []), 'view', 'contract'];
}
if ($process->opened_by !== null) {
$people[$process->opened_by] = [...($people[$process->opened_by] ?? []), 'view', 'review', 'interviews'];
}
foreach (DB::table('people_reviews as r')->join('people_applications as a', 'a.id', '=', 'r.people_application_id')->where('a.people_process_id', $process->id)->pluck('r.reviewed_by') as $person) {
$people[$person] = [...($people[$person] ?? []), 'view', 'review', 'interviews'];
}
if ($process->confirmed_by !== null) {
$people[$process->confirmed_by] = [...($people[$process->confirmed_by] ?? []), 'view', 'approve'];
}
foreach ($people as $person => $scopes) {
DB::table('people_process_access')->insert(['tenant_id' => $process->tenant_id, 'people_process_id' => $process->id, 'user_id' => $person,
'scopes' => json_encode(array_values(array_unique($scopes)), JSON_THROW_ON_ERROR), 'reason' => 'Preservação da participação anterior registada no processo.', 'created_at' => now(), 'updated_at' => now()]);
}
}
}
public function down(): void
{
throw new LogicException('Preservar atribuições e histórico de acesso.');
}
};
@@ -0,0 +1,55 @@
<?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
{
Schema::create('people_clarifications', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('people_process_id')->constrained()->restrictOnDelete();
$table->foreignId('people_application_id')->constrained()->restrictOnDelete();
$table->foreignId('people_review_id')->constrained()->restrictOnDelete();
$table->unsignedInteger('criterion_index');
$table->text('question');
$table->foreignId('raised_by')->constrained('users')->restrictOnDelete();
$table->foreignId('responsible_id')->constrained('users')->restrictOnDelete();
$table->date('due_on');
$table->uuid('operation_key');
$table->string('creation_hash', 64);
$table->string('status')->default('open');
$table->unsignedInteger('version')->default(1);
$table->json('responses');
$table->json('decisions');
$table->timestamps();
$table->unique(['people_review_id', 'criterion_index']);
$table->unique(['tenant_id', 'operation_key']);
});
if (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION sgc_preserve_people_clarification() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP = 'DELETE' OR OLD.status = 'resolved' THEN RAISE EXCEPTION 'Preservar esclarecimento e conclusao'; END IF;
IF NEW.version <> OLD.version + 1 OR ROW(NEW.tenant_id, NEW.people_process_id, NEW.people_application_id, NEW.people_review_id, NEW.criterion_index, NEW.question, NEW.raised_by, NEW.operation_key, NEW.creation_hash)
IS DISTINCT FROM ROW(OLD.tenant_id, OLD.people_process_id, OLD.people_application_id, OLD.people_review_id, OLD.criterion_index, OLD.question, OLD.raised_by, OLD.operation_key, OLD.creation_hash)
THEN RAISE EXCEPTION 'Preservar origem e versao do esclarecimento'; END IF;
RETURN NEW; END $$;
CREATE TRIGGER preserve_people_clarification BEFORE UPDATE OR DELETE ON people_clarifications FOR EACH ROW EXECUTE FUNCTION sgc_preserve_people_clarification();");
} elseif (DB::getDriverName() === 'sqlite') {
DB::unprepared("CREATE TRIGGER preserve_people_clarification_delete BEFORE DELETE ON people_clarifications BEGIN SELECT RAISE(ABORT, 'Preservar esclarecimento'); END;");
DB::unprepared("CREATE TRIGGER preserve_people_clarification_update BEFORE UPDATE ON people_clarifications WHEN OLD.status = 'resolved' OR NEW.version <> OLD.version + 1
OR NEW.tenant_id IS NOT OLD.tenant_id OR NEW.people_process_id IS NOT OLD.people_process_id OR NEW.people_application_id IS NOT OLD.people_application_id
OR NEW.people_review_id IS NOT OLD.people_review_id OR NEW.criterion_index IS NOT OLD.criterion_index OR NEW.question IS NOT OLD.question
OR NEW.raised_by IS NOT OLD.raised_by OR NEW.operation_key IS NOT OLD.operation_key OR NEW.creation_hash IS NOT OLD.creation_hash
BEGIN SELECT RAISE(ABORT, 'Preservar origem e conclusao'); END;");
}
}
public function down(): void
{
throw new LogicException('Preservar esclarecimentos e respostas.');
}
};
@@ -0,0 +1,20 @@
<?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('tenants', function (Blueprint $table) {
$table->json('intelligence_policy')->nullable();
});
}
public function down(): void
{
throw new LogicException('Preservar a parametrização e o histórico de auditoria.');
}
};
@@ -0,0 +1,60 @@
<?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
{
Schema::create('people_registry', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
$table->uuid('operation_key');
$table->string('creation_hash', 64);
$table->string('name', 200);
$table->string('reference', 120);
$table->string('reference_key', 120);
$table->text('profile_summary');
$table->boolean('active')->default(true);
$table->unsignedInteger('version')->default(1);
$table->timestamps();
$table->unique(['tenant_id', 'reference_key']);
$table->unique(['tenant_id', 'operation_key']);
$table->unique(['tenant_id', 'id']);
});
Schema::table('people_applications', function (Blueprint $table) {
$table->unsignedBigInteger('person_id')->nullable();
$table->json('person_snapshot')->nullable();
$table->foreign(['tenant_id', 'person_id'])->references(['tenant_id', 'id'])->on('people_registry')->restrictOnDelete();
$table->unique(['people_process_id', 'person_id']);
});
if (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION sgc_guard_people_registry() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP = 'DELETE' THEN RAISE EXCEPTION 'Preservar cadastro de pessoas'; END IF;
IF NEW.version <> OLD.version + 1 OR ROW(NEW.tenant_id,NEW.created_by,NEW.operation_key,NEW.creation_hash)
IS DISTINCT FROM ROW(OLD.tenant_id,OLD.created_by,OLD.operation_key,OLD.creation_hash)
THEN RAISE EXCEPTION 'Preservar origem e versao do cadastro'; END IF;
RETURN NEW; END $$;
CREATE TRIGGER guard_people_registry BEFORE UPDATE OR DELETE ON people_registry FOR EACH ROW EXECUTE FUNCTION sgc_guard_people_registry();");
} elseif (DB::getDriverName() === 'sqlite') {
DB::unprepared("CREATE TRIGGER guard_people_registry_delete BEFORE DELETE ON people_registry BEGIN SELECT RAISE(ABORT, 'Preservar cadastro'); END;
CREATE TRIGGER guard_people_registry_update BEFORE UPDATE ON people_registry WHEN NEW.version != OLD.version+1
OR NEW.tenant_id IS NOT OLD.tenant_id OR NEW.created_by IS NOT OLD.created_by OR NEW.operation_key IS NOT OLD.operation_key OR NEW.creation_hash IS NOT OLD.creation_hash
BEGIN SELECT RAISE(ABORT, 'Preservar origem e versao'); END;");
// SQLite rebuilds a table to add composite foreign keys: restore its evidence guards.
foreach (['UPDATE', 'DELETE'] as $operation) {
DB::unprepared("DROP TRIGGER IF EXISTS preserve_people_applications_{$operation};
CREATE TRIGGER preserve_people_applications_{$operation} BEFORE {$operation} ON people_applications BEGIN SELECT RAISE(ABORT, 'Preservar evidencias'); END;");
}
}
}
public function down(): void
{
throw new LogicException('Preservar cadastro, candidaturas e versões históricas.');
}
};
@@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (DB::getDriverName() === 'sqlite' && DB::table('sqlite_master')->where('type', 'trigger')->where('tbl_name', 'requisition_items')->exists()) {
throw new LogicException('Reconciliar protecções adicionais antes de reconstruir linhas SQLite.');
}
Schema::table('stock_items', fn (Blueprint $table) => $table->unique(['tenant_id', 'id']));
Schema::create('requisition_catalogue', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->unsignedBigInteger('stock_item_id');
$table->foreign(['tenant_id', 'stock_item_id'])->references(['tenant_id', 'id'])->on('stock_items')->restrictOnDelete();
$table->string('category', 100);
$table->string('purchase_type', 30);
$table->json('source_snapshot');
$table->boolean('published')->default(false);
$table->unsignedInteger('version')->default(1);
$table->timestamps();
$table->unique(['tenant_id', 'stock_item_id']);
$table->unique(['tenant_id', 'id']);
});
Schema::table('requisition_items', function (Blueprint $table) {
$table->unsignedBigInteger('catalogue_entry_id')->nullable();
$table->json('catalogue_snapshot')->nullable();
$table->foreign(['tenant_id', 'catalogue_entry_id'])->references(['tenant_id', 'id'])->on('requisition_catalogue')->restrictOnDelete();
});
if (DB::getDriverName() === 'sqlite') {
DB::unprepared("CREATE TRIGGER guard_requisition_catalogue_delete BEFORE DELETE ON requisition_catalogue BEGIN SELECT RAISE(ABORT, 'Preservar catalogo'); END;
CREATE TRIGGER guard_requisition_catalogue_update BEFORE UPDATE ON requisition_catalogue WHEN NEW.version != OLD.version+1 OR NEW.tenant_id IS NOT OLD.tenant_id OR NEW.stock_item_id IS NOT OLD.stock_item_id
BEGIN SELECT RAISE(ABORT, 'Preservar origem e versao do catalogo'); END;");
} elseif (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION sgc_guard_requisition_catalogue() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
IF TG_OP='DELETE' THEN RAISE EXCEPTION 'Preservar catalogo'; END IF;
IF NEW.version <> OLD.version+1 OR NEW.tenant_id IS DISTINCT FROM OLD.tenant_id OR NEW.stock_item_id IS DISTINCT FROM OLD.stock_item_id
THEN RAISE EXCEPTION 'Preservar origem e versao do catalogo'; END IF;
RETURN NEW; END $$;
CREATE TRIGGER guard_requisition_catalogue BEFORE UPDATE OR DELETE ON requisition_catalogue FOR EACH ROW EXECUTE FUNCTION sgc_guard_requisition_catalogue();");
}
}
public function down(): void
{
throw new LogicException('Preservar catálogo e fontes históricas dos pedidos.');
}
};
@@ -0,0 +1,37 @@
<?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
{
Schema::create('submission_forms', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
$table->string('audience', 30);
$table->string('modality', 30);
$table->unsignedInteger('version');
$table->json('definition');
$table->text('reason');
$table->timestamp('created_at');
$table->unique(['tenant_id', 'audience', 'modality', 'version']);
});
if (DB::getDriverName() === 'sqlite') {
DB::unprepared("CREATE TRIGGER preserve_submission_forms_update BEFORE UPDATE ON submission_forms BEGIN SELECT RAISE(ABORT, 'Preservar versao do formulario'); END;
CREATE TRIGGER preserve_submission_forms_delete BEFORE DELETE ON submission_forms BEGIN SELECT RAISE(ABORT, 'Preservar versao do formulario'); END;");
} elseif (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION sgc_preserve_submission_forms() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Preservar versao do formulario'; END $$;
CREATE TRIGGER preserve_submission_forms BEFORE UPDATE OR DELETE ON submission_forms FOR EACH ROW EXECUTE FUNCTION sgc_preserve_submission_forms();");
}
}
public function down(): void
{
throw new LogicException('Preservar versoes dos formularios.');
}
};
@@ -0,0 +1,73 @@
<?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
{
Schema::create('candidate_portal_versions', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
$table->unsignedInteger('version');
$table->boolean('registration_open');
$table->text('notice');
$table->text('reason');
$table->timestamp('created_at');
$table->unique(['tenant_id', 'version']);
$table->unique(['tenant_id', 'id']);
});
Schema::create('candidate_accounts', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->string('email', 254);
$table->string('password');
$table->unsignedBigInteger('portal_version_id');
$table->foreign(['tenant_id', 'portal_version_id'])->references(['tenant_id', 'id'])->on('candidate_portal_versions')->restrictOnDelete();
$table->timestamps();
$table->unique(['tenant_id', 'email']);
$table->unique(['tenant_id', 'id']);
});
Schema::create('candidate_profile_versions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->unsignedBigInteger('candidate_account_id');
$table->foreign(['tenant_id', 'candidate_account_id'])->references(['tenant_id', 'id'])->on('candidate_accounts')->restrictOnDelete();
$table->unsignedInteger('version');
$table->json('data');
$table->text('reason');
$table->timestamp('created_at');
$table->unique(['candidate_account_id', 'version']);
});
Schema::create('candidate_events', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->unsignedBigInteger('candidate_account_id');
$table->foreign(['tenant_id', 'candidate_account_id'])->references(['tenant_id', 'id'])->on('candidate_accounts')->restrictOnDelete();
$table->string('action', 80);
$table->json('metadata');
$table->timestamp('created_at');
});
if (DB::getDriverName() === 'sqlite') {
foreach (['candidate_portal_versions', 'candidate_profile_versions', 'candidate_events'] as $table) {
// Table names are the fixed allowlist above.
DB::unprepared("CREATE TRIGGER preserve_{$table}_update BEFORE UPDATE ON {$table} BEGIN SELECT RAISE(ABORT, 'Preservar historico do candidato'); END;
CREATE TRIGGER preserve_{$table}_delete BEFORE DELETE ON {$table} BEGIN SELECT RAISE(ABORT, 'Preservar historico do candidato'); END;");
}
} elseif (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION sgc_preserve_candidate_versions() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Preservar historico do candidato'; END $$;
CREATE TRIGGER preserve_candidate_portal_versions BEFORE UPDATE OR DELETE ON candidate_portal_versions FOR EACH ROW EXECUTE FUNCTION sgc_preserve_candidate_versions();
CREATE TRIGGER preserve_candidate_profile_versions BEFORE UPDATE OR DELETE ON candidate_profile_versions FOR EACH ROW EXECUTE FUNCTION sgc_preserve_candidate_versions();
CREATE TRIGGER preserve_candidate_events BEFORE UPDATE OR DELETE ON candidate_events FOR EACH ROW EXECUTE FUNCTION sgc_preserve_candidate_versions();");
}
}
public function down(): void
{
throw new LogicException('Preservar perfis e versões dos candidatos.');
}
};
@@ -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
{
Schema::table('candidate_accounts', function (Blueprint $table) {
$table->timestamp('email_verified_at')->nullable();
$table->unsignedInteger('session_version')->default(1);
});
Schema::create('candidate_access_tokens', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->unsignedBigInteger('candidate_account_id');
$table->foreign(['tenant_id', 'candidate_account_id'])->references(['tenant_id', 'id'])->on('candidate_accounts')->restrictOnDelete();
$table->string('purpose', 20);
$table->string('token_hash', 64)->unique();
$table->timestamp('expires_at');
$table->timestamp('consumed_at')->nullable();
$table->timestamp('created_at');
});
}
public function down(): void
{
throw new LogicException('Preservar estado de verificação e recuperação.');
}
};
@@ -0,0 +1,57 @@
<?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
{
Schema::table('people_processes', fn (Blueprint $table) => $table->unique(['tenant_id', 'id']));
Schema::create('candidate_opportunity_publications', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->unsignedBigInteger('people_process_id');
$table->foreign(['tenant_id', 'people_process_id'])->references(['tenant_id', 'id'])->on('people_processes')->restrictOnDelete();
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
$table->unsignedInteger('version');
$table->boolean('active');
$table->text('description');
$table->text('reason');
$table->timestamp('created_at');
$table->unique(['people_process_id', 'version']);
});
Schema::create('candidate_submissions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->unsignedBigInteger('people_process_id');
$table->unsignedBigInteger('candidate_account_id');
$table->foreign(['tenant_id', 'people_process_id'])->references(['tenant_id', 'id'])->on('people_processes')->restrictOnDelete();
$table->foreign(['tenant_id', 'candidate_account_id'])->references(['tenant_id', 'id'])->on('candidate_accounts')->restrictOnDelete();
$table->uuid('operation_key');
$table->string('reference', 50)->unique();
$table->string('input_hash', 64);
$table->json('snapshot');
$table->timestamp('created_at');
$table->unique(['people_process_id', 'candidate_account_id']);
$table->unique(['candidate_account_id', 'operation_key']);
});
if (DB::getDriverName() === 'sqlite') {
foreach (['candidate_opportunity_publications', 'candidate_submissions'] as $table) {
DB::unprepared("CREATE TRIGGER preserve_{$table}_update BEFORE UPDATE ON {$table} BEGIN SELECT RAISE(ABORT, 'Preservar submissao e publicacao'); END;
CREATE TRIGGER preserve_{$table}_delete BEFORE DELETE ON {$table} BEGIN SELECT RAISE(ABORT, 'Preservar submissao e publicacao'); END;");
}
} elseif (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION sgc_preserve_candidate_submissions() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Preservar submissao e publicacao'; END $$;
CREATE TRIGGER preserve_candidate_publications BEFORE UPDATE OR DELETE ON candidate_opportunity_publications FOR EACH ROW EXECUTE FUNCTION sgc_preserve_candidate_submissions();
CREATE TRIGGER preserve_candidate_submissions BEFORE UPDATE OR DELETE ON candidate_submissions FOR EACH ROW EXECUTE FUNCTION sgc_preserve_candidate_submissions();");
}
}
public function down(): void
{
throw new LogicException('Preservar candidaturas e publicações.');
}
};
@@ -0,0 +1,45 @@
<?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
{
Schema::table('candidate_submissions', fn (Blueprint $table) => $table->unique(['tenant_id', 'people_process_id', 'id'], 'candidate_submission_process_identity'));
Schema::table('people_applications', fn (Blueprint $table) => $table->unique(['tenant_id', 'people_process_id', 'id'], 'people_application_process_identity'));
Schema::create('candidate_triage', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->unsignedBigInteger('people_process_id');
$table->unsignedBigInteger('candidate_submission_id')->unique();
$table->unsignedBigInteger('people_application_id')->nullable()->unique();
$table->foreign(['tenant_id', 'people_process_id', 'candidate_submission_id'], 'triage_submission_scope')->references(['tenant_id', 'people_process_id', 'id'])->on('candidate_submissions')->restrictOnDelete();
$table->foreign(['tenant_id', 'people_process_id', 'people_application_id'], 'triage_application_scope')->references(['tenant_id', 'people_process_id', 'id'])->on('people_applications')->restrictOnDelete();
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
$table->uuid('operation_key');
$table->string('input_hash', 64);
$table->string('outcome', 30);
$table->unsignedInteger('profile_version');
$table->text('reason');
$table->text('public_message');
$table->timestamp('created_at');
$table->unique(['tenant_id', 'operation_key']);
});
if (DB::getDriverName() === 'sqlite') {
DB::unprepared("CREATE TRIGGER preserve_candidate_triage_update BEFORE UPDATE ON candidate_triage BEGIN SELECT RAISE(ABORT, 'Preservar decisao de triagem'); END;
CREATE TRIGGER preserve_candidate_triage_delete BEFORE DELETE ON candidate_triage BEGIN SELECT RAISE(ABORT, 'Preservar decisao de triagem'); END;");
} elseif (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION sgc_preserve_candidate_triage() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Preservar decisao de triagem'; END $$;
CREATE TRIGGER preserve_candidate_triage BEFORE UPDATE OR DELETE ON candidate_triage FOR EACH ROW EXECUTE FUNCTION sgc_preserve_candidate_triage();");
}
}
public function down(): void
{
throw new LogicException('Preservar decisões e origens das candidaturas.');
}
};