This commit is contained in:
2026-11-02 08:08:05 +02:00
parent 13a6cb9922
commit 504b01b244
246 changed files with 24499 additions and 185 deletions
@@ -0,0 +1,48 @@
<?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::connection(DB::getDefaultConnection())->create('supply_records', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained('tenants')->restrictOnDelete();
$table->string('kind', 20);
$table->uuid('record_key');
$table->unsignedInteger('version');
$table->string('state', 30);
$table->foreignId('stock_item_id')->nullable()->constrained()->restrictOnDelete();
$table->foreignId('warehouse_id')->nullable()->constrained()->restrictOnDelete();
$table->foreignId('supplier_id')->nullable()->constrained()->restrictOnDelete();
$table->foreignId('purchase_order_id')->nullable()->constrained()->restrictOnDelete();
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
$table->json('data');
$table->json('snapshot');
$table->text('reason');
$table->uuid('operation_key');
$table->string('input_hash', 64);
$table->timestamp('created_at');
$table->unique(['tenant_id', 'kind', 'record_key', 'version'], 'supply_record_version');
$table->unique(['tenant_id', 'operation_key']);
$table->index(['tenant_id', 'kind', 'stock_item_id', 'warehouse_id']);
$table->index(['tenant_id', 'purchase_order_id']);
});
if (DB::getDriverName() === 'sqlite') {
DB::unprepared("CREATE TRIGGER supply_records_update BEFORE UPDATE ON supply_records BEGIN SELECT RAISE(ABORT, 'Preservar historico de fornecimento'); END;
CREATE TRIGGER supply_records_delete BEFORE DELETE ON supply_records BEGIN SELECT RAISE(ABORT, 'Preservar historico de fornecimento'); END;");
} elseif (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION preserve_supply_records() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Preservar historico de fornecimento'; END $$;
CREATE TRIGGER supply_records_immutable BEFORE UPDATE OR DELETE ON supply_records FOR EACH ROW EXECUTE FUNCTION preserve_supply_records();");
}
}
public function down(): void
{
throw new LogicException('Preservar fornecimentos, politicas e processos de importacao.');
}
};
@@ -0,0 +1,70 @@
<?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 = Schema::connection(DB::getDefaultConnection());
$schema->table('supply_records', function (Blueprint $table) {
$table->unique(['tenant_id', 'id', 'stock_item_id'], 'lot_record_item_scope');
$table->unique(['tenant_id', 'id', 'stock_item_id', 'warehouse_id'], 'lot_record_location_scope');
});
$schema->table('stock_movements', fn (Blueprint $table) => $table->unique(['tenant_id', 'id', 'stock_item_id', 'warehouse_id'], 'lot_movement_location_scope'));
$schema->create('inventory_lot_allocations', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->unsignedBigInteger('stock_item_id');
$table->unsignedBigInteger('warehouse_id');
$table->unsignedBigInteger('lot_id');
$table->unsignedBigInteger('stock_movement_id')->nullable();
$table->unsignedBigInteger('setup_record_id')->nullable();
$table->bigInteger('quantity_milli');
$table->timestamp('created_at');
$table->foreign(['tenant_id', 'lot_id', 'stock_item_id'], 'allocation_lot_scope')->references(['tenant_id', 'id', 'stock_item_id'])->on('supply_records')->restrictOnDelete();
$table->foreign(['tenant_id', 'stock_movement_id', 'stock_item_id', 'warehouse_id'], 'allocation_movement_scope')->references(['tenant_id', 'id', 'stock_item_id', 'warehouse_id'])->on('stock_movements')->restrictOnDelete();
$table->foreign(['tenant_id', 'setup_record_id', 'stock_item_id', 'warehouse_id'], 'allocation_setup_scope')->references(['tenant_id', 'id', 'stock_item_id', 'warehouse_id'])->on('supply_records')->restrictOnDelete();
$table->unique(['stock_movement_id', 'lot_id'], 'allocation_movement_once');
$table->unique(['setup_record_id', 'lot_id'], 'allocation_baseline_once');
$table->index(['tenant_id', 'stock_item_id', 'warehouse_id']);
});
if (DB::getDriverName() === 'sqlite') {
DB::unprepared("CREATE TRIGGER allocation_insert BEFORE INSERT ON inventory_lot_allocations BEGIN
SELECT CASE WHEN NEW.quantity_milli = 0 OR (NEW.stock_movement_id IS NULL) = (NEW.setup_record_id IS NULL) THEN RAISE(ABORT, 'Origem unica da reparticao obrigatoria') END;
SELECT CASE WHEN NOT EXISTS(SELECT 1 FROM supply_records WHERE id=NEW.lot_id AND kind='lot' AND version=1) THEN RAISE(ABORT, 'Identidade de lote invalida') END;
SELECT CASE WHEN COALESCE((SELECT SUM(quantity_milli) FROM inventory_lot_allocations WHERE lot_id=NEW.lot_id AND warehouse_id=NEW.warehouse_id),0)+NEW.quantity_milli < 0 THEN RAISE(ABORT, 'Saldo de lote negativo') END;
SELECT CASE WHEN NEW.stock_movement_id IS NOT NULL AND ((SELECT quantity_milli FROM stock_movements WHERE id=NEW.stock_movement_id)*NEW.quantity_milli<=0 OR COALESCE((SELECT SUM(ABS(quantity_milli)) FROM inventory_lot_allocations WHERE stock_movement_id=NEW.stock_movement_id),0)+ABS(NEW.quantity_milli)>ABS((SELECT quantity_milli FROM stock_movements WHERE id=NEW.stock_movement_id))) THEN RAISE(ABORT, 'Reparticao excede movimento') END;
SELECT CASE WHEN NEW.setup_record_id IS NOT NULL AND (NEW.quantity_milli<0 OR NOT EXISTS(SELECT 1 FROM supply_records WHERE id=NEW.setup_record_id AND kind='lot_setup' AND state='active') OR COALESCE((SELECT SUM(quantity_milli) FROM inventory_lot_allocations WHERE setup_record_id=NEW.setup_record_id),0)+NEW.quantity_milli>(SELECT CAST(json_extract(snapshot,'$.balance.quantity') AS INTEGER) FROM supply_records WHERE id=NEW.setup_record_id)) THEN RAISE(ABORT, 'Reparticao excede saldo conferido') END;
END;
CREATE TRIGGER allocation_update BEFORE UPDATE ON inventory_lot_allocations BEGIN SELECT RAISE(ABORT, 'Preservar reparticoes de lotes'); END;
CREATE TRIGGER allocation_delete BEFORE DELETE ON inventory_lot_allocations BEGIN SELECT RAISE(ABORT, 'Preservar reparticoes de lotes'); END;");
} elseif (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION preserve_inventory_lot_allocations() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE source_quantity numeric; baseline_quantity numeric;
BEGIN
IF TG_OP <> 'INSERT' THEN RAISE EXCEPTION 'Preservar reparticoes de lotes'; END IF;
IF NEW.quantity_milli=0 OR (NEW.stock_movement_id IS NULL)=(NEW.setup_record_id IS NULL) THEN RAISE EXCEPTION 'Origem unica da reparticao obrigatoria'; END IF;
IF NOT EXISTS(SELECT 1 FROM supply_records WHERE id=NEW.lot_id AND kind='lot' AND version=1) THEN RAISE EXCEPTION 'Identidade de lote invalida'; END IF;
PERFORM id FROM supply_records WHERE id=NEW.lot_id FOR UPDATE;
IF COALESCE((SELECT SUM(quantity_milli) FROM inventory_lot_allocations WHERE lot_id=NEW.lot_id AND warehouse_id=NEW.warehouse_id),0)+NEW.quantity_milli<0 THEN RAISE EXCEPTION 'Saldo de lote negativo'; END IF;
IF NEW.stock_movement_id IS NOT NULL THEN
SELECT quantity_milli INTO source_quantity FROM stock_movements WHERE id=NEW.stock_movement_id FOR UPDATE;
IF source_quantity*NEW.quantity_milli<=0 OR COALESCE((SELECT SUM(ABS(quantity_milli)) FROM inventory_lot_allocations WHERE stock_movement_id=NEW.stock_movement_id),0)+ABS(NEW.quantity_milli)>ABS(source_quantity) THEN RAISE EXCEPTION 'Reparticao excede movimento'; END IF;
ELSE
SELECT (snapshot->'balance'->>'quantity')::numeric INTO baseline_quantity FROM supply_records WHERE id=NEW.setup_record_id AND kind='lot_setup' AND state='active' FOR UPDATE;
IF baseline_quantity IS NULL OR NEW.quantity_milli<0 OR COALESCE((SELECT SUM(quantity_milli) FROM inventory_lot_allocations WHERE setup_record_id=NEW.setup_record_id),0)+NEW.quantity_milli>baseline_quantity THEN RAISE EXCEPTION 'Reparticao excede saldo conferido'; END IF;
END IF;
RETURN NEW; END $$;
CREATE TRIGGER preserve_lot_allocations BEFORE INSERT OR UPDATE OR DELETE ON inventory_lot_allocations FOR EACH ROW EXECUTE FUNCTION preserve_inventory_lot_allocations();");
}
}
public function down(): void
{
throw new LogicException('Preservar lotes, origens e reparticoes do razao.');
}
};
@@ -0,0 +1,22 @@
<?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::connection(DB::getDefaultConnection())->table('stock_movements', function (Blueprint $table) {
$table->dropUnique(['stock_operation_id', 'warehouse_id']);
$table->unique(['stock_operation_id', 'warehouse_id', 'movement_type'], 'stock_operation_location_type_once');
});
}
public function down(): void
{
throw new LogicException('Preservar os movimentos de ajuste e o histórico de lotes.');
}
};
@@ -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('tenants', function (Blueprint $table) {
$table->json('work_context_policy')->nullable();
});
Schema::table('organization_profiles', function (Blueprint $table) {
$table->json('work_contexts')->nullable();
});
foreach (['requisitions' => 'procurement.internal', 'people_processes' => 'people.internal'] as $name => $key) {
Schema::table($name, function (Blueprint $table) use ($key) {
$table->string('work_context_key', 40)->default($key)->index();
});
if (DB::getDriverName() === 'sqlite') {
DB::unprepared("CREATE TRIGGER {$name}_context_immutable BEFORE UPDATE OF work_context_key ON {$name} WHEN NEW.work_context_key != OLD.work_context_key BEGIN SELECT RAISE(ABORT, 'Process context is immutable'); END");
} elseif (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION {$name}_context_immutable() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN IF NEW.work_context_key IS DISTINCT FROM OLD.work_context_key THEN RAISE EXCEPTION 'Process context is immutable'; END IF; RETURN NEW; END; \$\$; CREATE TRIGGER {$name}_context_immutable BEFORE UPDATE ON {$name} FOR EACH ROW EXECUTE FUNCTION {$name}_context_immutable();");
}
}
}
public function down(): void
{
foreach (['requisitions', 'people_processes'] as $name) {
DB::unprepared("DROP TRIGGER IF EXISTS {$name}_context_immutable".(DB::getDriverName() === 'pgsql' ? " ON {$name}" : ''));
if (DB::getDriverName() === 'pgsql') {
DB::unprepared("DROP FUNCTION IF EXISTS {$name}_context_immutable()");
}
Schema::table($name, function (Blueprint $table) {
$table->dropIndex(['work_context_key']);
$table->dropColumn('work_context_key');
});
}
Schema::table('organization_profiles', fn (Blueprint $table) => $table->dropColumn('work_contexts'));
Schema::table('tenants', fn (Blueprint $table) => $table->dropColumn('work_context_policy'));
}
};
@@ -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('requisitions', fn (Blueprint $t) => $t->unsignedInteger('process_version')->default(1));
Schema::create('requisition_commands', function (Blueprint $t) {
$t->id();
$t->unsignedBigInteger('tenant_id');
$t->unsignedBigInteger('actor_id');
$t->uuid('operation_key');
$t->string('request_hash', 64);
$t->json('result');
$t->timestamp('created_at');
$t->unique(['tenant_id', 'actor_id', 'operation_key']);
});
foreach (['update', 'delete'] as $event) {
if (DB::getDriverName() === 'sqlite') {
DB::unprepared("CREATE TRIGGER rq_command_{$event} BEFORE {$event} ON requisition_commands BEGIN SELECT RAISE(ABORT, 'Command receipts are immutable'); END");
} elseif (DB::getDriverName() === 'pgsql') {
DB::unprepared("CREATE FUNCTION rq_command_{$event}() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'Command receipts are immutable'; END; \$\$; CREATE TRIGGER rq_command_{$event} BEFORE {$event} ON requisition_commands FOR EACH ROW EXECUTE FUNCTION rq_command_{$event}();");
}
}
}
public function down(): void
{
if (DB::table('requisition_commands')->exists()) {
throw new RuntimeException('Não apagar recibos de operações. Recuperar por migração compatível ou restauro ensaiado.');
}
Schema::drop('requisition_commands');
if (DB::getDriverName() === 'pgsql') {
foreach (['update', 'delete'] as $event) {
DB::unprepared("DROP FUNCTION IF EXISTS rq_command_{$event}()");
}
}
Schema::table('requisitions', fn (Blueprint $t) => $t->dropColumn('process_version'));
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('workflow_notices', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('requisition_id')->constrained()->restrictOnDelete();
$table->foreignId('approval_step_id')->constrained()->restrictOnDelete();
$table->foreignId('recipient_id')->constrained('users')->restrictOnDelete();
$table->string('reference');
$table->string('role');
$table->timestampTz('created_at');
$table->timestampTz('read_at')->nullable();
$table->unique(['approval_step_id', 'recipient_id']);
$table->index(['tenant_id', 'recipient_id', 'read_at']);
});
}
public function down(): void
{
Schema::dropIfExists('workflow_notices');
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('warehouse_requests', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
$table->foreignId('warehouse_id')->constrained()->restrictOnDelete();
$table->foreignId('cost_center_id')->constrained()->restrictOnDelete();
$table->string('work_context_key')->default('procurement.internal');
$table->uuid('operation_key');
$table->string('input_hash', 64);
$table->string('status')->default('draft');
$table->json('snapshot');
$table->timestampTz('created_at');
$table->unique(['tenant_id', 'created_by', 'operation_key']);
});
}
public function down(): void
{
Schema::dropIfExists('warehouse_requests');
}
};
@@ -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::create('warehouse_request_events', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('warehouse_request_id')->constrained()->restrictOnDelete();
$table->foreignId('actor_id')->constrained('users')->restrictOnDelete();
$table->unsignedInteger('version');
$table->uuid('operation_key');
$table->string('input_hash', 64);
$table->string('action');
$table->json('data');
$table->text('reason');
$table->timestampTz('created_at');
$table->unique(['warehouse_request_id', 'version']);
$table->unique(['tenant_id', 'actor_id', 'operation_key']);
});
}
public function down(): void
{
Schema::dropIfExists('warehouse_request_events');
}
};
@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration {
public function up(): void {
Schema::create('warehouse_reservations',function(Blueprint $table) {
$table->id(); $table->foreignId('tenant_id')->constrained()->restrictOnDelete();
$table->foreignId('warehouse_request_id')->constrained()->restrictOnDelete();
$table->unsignedInteger('line_index'); $table->foreignId('stock_item_id')->constrained()->restrictOnDelete();
$table->foreignId('warehouse_id')->constrained()->restrictOnDelete();
$table->foreignId('authorization_event_id')->constrained('warehouse_request_events')->restrictOnDelete();
$table->foreignId('actor_id')->constrained('users')->restrictOnDelete();
$table->uuid('command_key'); $table->string('kind'); $table->bigInteger('quantity_milli'); $table->timestampTz('created_at');
$table->unique(['warehouse_request_id','line_index','command_key','kind'],'warehouse_reservation_command_unique');
$table->index(['tenant_id','stock_item_id','warehouse_id']);
});
if(DB::getDriverName()==='sqlite')DB::unprepared("CREATE TRIGGER warehouse_reservations_update BEFORE UPDATE ON warehouse_reservations BEGIN SELECT RAISE(ABORT, 'Preservar reservas'); END; CREATE TRIGGER warehouse_reservations_delete BEFORE DELETE ON warehouse_reservations BEGIN SELECT RAISE(ABORT, 'Preservar reservas'); END;");
if(DB::getDriverName()==='pgsql')DB::unprepared("CREATE FUNCTION preserve_warehouse_reservations() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Preservar reservas'; END $$; CREATE TRIGGER warehouse_reservations_immutable BEFORE UPDATE OR DELETE ON warehouse_reservations FOR EACH ROW EXECUTE FUNCTION preserve_warehouse_reservations();");
}
public function down(): void { Schema::dropIfExists('warehouse_reservations'); }
};