Preserva entrega A aceite de servicos em rascunho
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
<?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 (['clients', 'services', 'service_drafts', 'billing_drafts'] as $area) {
|
||||
Schema::create('a_'.$area, function (Blueprint $table) use ($area): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->unsignedBigInteger('company_id');
|
||||
$table->unsignedBigInteger('unit_id')->nullable();
|
||||
$table->foreign(['tenant_id', 'company_id'])->references(['tenant_id', 'id'])->on('companies')->restrictOnDelete();
|
||||
$table->foreign(['tenant_id', 'company_id', 'unit_id'])->references(['tenant_id', 'company_id', 'id'])->on('units')->restrictOnDelete();
|
||||
$table->string('name');
|
||||
$table->unsignedInteger('version')->default(1);
|
||||
$table->jsonb('data');
|
||||
if (in_array($area, ['service_drafts', 'billing_drafts'], true)) {
|
||||
$table->unsignedBigInteger('client_id');
|
||||
$table->foreign(['tenant_id', 'company_id', 'client_id'])->references(['tenant_id', 'company_id', 'id'])->on('a_clients')->restrictOnDelete();
|
||||
$table->string('state')->default('draft');
|
||||
}
|
||||
if ($area === 'billing_drafts') {
|
||||
$table->unsignedBigInteger('source_id');
|
||||
$table->foreign(['tenant_id', 'company_id', 'unit_id', 'source_id'])->references(['tenant_id', 'company_id', 'unit_id', 'id'])->on('a_service_drafts')->restrictOnDelete();
|
||||
}
|
||||
$table->timestampsTz();
|
||||
$table->unique(['tenant_id', 'company_id', 'id']);
|
||||
$table->unique(['tenant_id', 'company_id', 'unit_id', 'id']);
|
||||
});
|
||||
$unitCheck = in_array($area, ['service_drafts', 'billing_drafts'], true) ? 'IS NOT NULL' : 'IS NULL';
|
||||
DB::statement("ALTER TABLE a_$area ADD CHECK (unit_id $unitCheck AND version > 0 AND jsonb_typeof(data) = 'object')");
|
||||
if (in_array($area, ['service_drafts', 'billing_drafts'], true)) { DB::statement("ALTER TABLE a_$area ADD CHECK (state = 'draft')"); }
|
||||
}
|
||||
Schema::create('a_history', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id'); $table->unsignedBigInteger('company_id'); $table->unsignedBigInteger('unit_id')->nullable();
|
||||
$table->string('area', 30); $table->unsignedBigInteger('record_id'); $table->unsignedInteger('version');
|
||||
$table->unsignedBigInteger('actor_id'); $table->jsonb('snapshot'); $table->timestampTz('created_at')->useCurrent();
|
||||
$table->unique(['area', 'record_id', 'version']);
|
||||
$table->foreign(['tenant_id', 'company_id'])->references(['tenant_id', 'id'])->on('companies')->restrictOnDelete();
|
||||
});
|
||||
Schema::create('a_events', function (Blueprint $table): void {
|
||||
$table->id(); $table->foreignId('history_id')->unique()->constrained('a_history')->restrictOnDelete();
|
||||
$table->string('type', 80); $table->unsignedInteger('schema_version')->default(1);
|
||||
$table->jsonb('payload'); $table->timestampTz('created_at')->useCurrent();
|
||||
});
|
||||
Schema::create('a_outbox', function (Blueprint $table): void {
|
||||
$table->foreignId('event_id')->primary()->constrained('a_events')->restrictOnDelete();
|
||||
$table->string('state')->default('captured'); $table->timestampTz('created_at')->useCurrent();
|
||||
});
|
||||
DB::statement("ALTER TABLE a_outbox ADD CHECK (state = 'captured')");
|
||||
Schema::create('a_pending', function (Blueprint $table): void {
|
||||
$table->id(); $table->foreignId('history_id')->constrained('a_history')->restrictOnDelete();
|
||||
$table->string('code', 80); $table->string('message', 500); $table->timestampTz('created_at')->useCurrent();
|
||||
$table->unique(['history_id', 'code']);
|
||||
});
|
||||
Schema::create('a_commands', function (Blueprint $table): void {
|
||||
$table->id(); $table->unsignedBigInteger('tenant_id'); $table->unsignedBigInteger('company_id');
|
||||
$table->unsignedBigInteger('actor_id'); $table->string('command_key', 100); $table->string('request_hash', 64);
|
||||
$table->jsonb('result'); $table->timestampTz('created_at')->useCurrent();
|
||||
$table->unique(['tenant_id', 'company_id', 'command_key']);
|
||||
});
|
||||
DB::unprepared(<<<'SQL'
|
||||
CREATE OR REPLACE FUNCTION sgf_a_record_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE line jsonb; field text; value text;
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' AND NEW.version <> 1 THEN RAISE EXCEPTION 'SGF_A_INITIAL_VERSION'; END IF;
|
||||
IF TG_OP = 'UPDATE' THEN
|
||||
IF NEW.id IS DISTINCT FROM OLD.id OR NEW.tenant_id IS DISTINCT FROM OLD.tenant_id OR NEW.company_id IS DISTINCT FROM OLD.company_id OR NEW.unit_id IS DISTINCT FROM OLD.unit_id THEN
|
||||
RAISE EXCEPTION 'SGF_A_IMMUTABLE_SCOPE';
|
||||
END IF;
|
||||
IF NEW.version <> OLD.version + 1 THEN RAISE EXCEPTION 'SGF_A_VERSION_REQUIRED'; END IF;
|
||||
IF TG_TABLE_NAME = 'a_billing_drafts' THEN
|
||||
IF NEW.source_id IS DISTINCT FROM OLD.source_id THEN RAISE EXCEPTION 'SGF_A_IMMUTABLE_SOURCE'; END IF;
|
||||
END IF;
|
||||
END IF;
|
||||
IF TG_TABLE_NAME IN ('a_service_drafts','a_billing_drafts') THEN
|
||||
IF (NEW.data - ARRAY['description','currency','lines','client_name','source_version','tax_status','accounting_status']) <> '{}'::jsonb THEN RAISE EXCEPTION 'SGF_A_UNSUPPORTED_DATA'; END IF;
|
||||
IF NEW.data->>'tax_status' IS DISTINCT FROM 'pending' OR NEW.data->>'accounting_status' IS DISTINCT FROM 'not_processed' THEN RAISE EXCEPTION 'SGF_A_DRAFT_ONLY'; END IF;
|
||||
IF NEW.data->>'currency' IS NOT NULL AND NEW.data->>'currency' !~ '^[A-Z]{3}$' THEN RAISE EXCEPTION 'SGF_A_CURRENCY'; END IF;
|
||||
IF jsonb_typeof(NEW.data->'lines') IS DISTINCT FROM 'array' OR jsonb_array_length(NEW.data->'lines') < 1 THEN RAISE EXCEPTION 'SGF_A_LINES'; END IF;
|
||||
FOR line IN SELECT * FROM jsonb_array_elements(NEW.data->'lines') LOOP
|
||||
IF (line - ARRAY['service_id','service_name','description','quantity','unit_price']) <> '{}'::jsonb THEN RAISE EXCEPTION 'SGF_A_UNSUPPORTED_LINE_DATA'; END IF;
|
||||
IF NOT EXISTS(SELECT 1 FROM a_services s WHERE s.id = (line->>'service_id')::bigint AND s.tenant_id = NEW.tenant_id AND s.company_id = NEW.company_id) THEN RAISE EXCEPTION 'SGF_A_FOREIGN_SERVICE'; END IF;
|
||||
FOREACH field IN ARRAY ARRAY['quantity','unit_price'] LOOP
|
||||
value := line->>field;
|
||||
IF value IS NOT NULL THEN
|
||||
IF jsonb_typeof(line->field) <> 'string' OR value !~ '^(0|[1-9][0-9]{0,15})(\.[0-9]{1,8})?$' THEN RAISE EXCEPTION 'SGF_A_DECIMAL_PRECISION'; END IF;
|
||||
IF field = 'quantity' AND value::numeric <= 0 THEN RAISE EXCEPTION 'SGF_A_QUANTITY'; END IF;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END; $$;
|
||||
CREATE OR REPLACE FUNCTION sgf_a_history_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE item record; target text;
|
||||
BEGIN
|
||||
target := CASE NEW.area WHEN 'clients' THEN 'a_clients' WHEN 'services' THEN 'a_services' WHEN 'service_drafts' THEN 'a_service_drafts' WHEN 'billing_drafts' THEN 'a_billing_drafts' ELSE NULL END;
|
||||
IF target IS NULL THEN RAISE EXCEPTION 'SGF_A_AREA'; END IF;
|
||||
EXECUTE format('SELECT tenant_id,company_id,unit_id,version FROM %I WHERE id=$1', target) INTO item USING NEW.record_id;
|
||||
IF item IS NULL OR item.tenant_id IS DISTINCT FROM NEW.tenant_id OR item.company_id IS DISTINCT FROM NEW.company_id OR item.unit_id IS DISTINCT FROM NEW.unit_id OR item.version IS DISTINCT FROM NEW.version THEN RAISE EXCEPTION 'SGF_A_HISTORY_SCOPE'; END IF;
|
||||
RETURN NEW;
|
||||
END; $$;
|
||||
SQL);
|
||||
foreach (['clients', 'services', 'service_drafts', 'billing_drafts'] as $area) {
|
||||
DB::statement("CREATE TRIGGER a_{$area}_guard BEFORE INSERT OR UPDATE ON a_$area FOR EACH ROW EXECUTE FUNCTION sgf_a_record_guard()");
|
||||
}
|
||||
DB::statement('CREATE TRIGGER a_history_scope BEFORE INSERT ON a_history FOR EACH ROW EXECUTE FUNCTION sgf_a_history_guard()');
|
||||
foreach (['a_history', 'a_events', 'a_outbox', 'a_pending', 'a_commands'] as $table) {
|
||||
DB::statement("CREATE TRIGGER {$table}_append_only BEFORE UPDATE OR DELETE ON $table FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()");
|
||||
}
|
||||
$company = ['company.view','company.edit','unit.create','access.manage']; $unit = ['unit.view','unit.edit'];
|
||||
foreach (['clients','services','service_drafts','billing_drafts'] as $area) {
|
||||
foreach (['view','create','edit','manage'] as $action) {
|
||||
if (in_array($area, ['clients','services'], true)) { $company[] = "$area.$action"; } else { $unit[] = "$area.$action"; }
|
||||
}
|
||||
}
|
||||
$companySql = "'".implode("','", $company)."'"; $unitSql = "'".implode("','", $unit)."'";
|
||||
DB::statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check');
|
||||
DB::statement("ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK ((permission IN ($companySql) AND unit_id IS NULL) OR (permission IN ($unitSql) AND unit_id IS NOT NULL))");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::table('permission_grants')->whereNotIn('permission', ['company.view','company.edit','unit.create','access.manage','unit.view','unit.edit'])->exists()) {
|
||||
throw new RuntimeException('Remove explicit A grants through an approved procedure before rolling back.');
|
||||
}
|
||||
foreach (['a_commands','a_pending','a_outbox','a_events','a_history','a_billing_drafts','a_service_drafts','a_services','a_clients'] as $table) { Schema::dropIfExists($table); }
|
||||
DB::statement('DROP FUNCTION IF EXISTS sgf_a_record_guard()'); DB::statement('DROP FUNCTION IF EXISTS sgf_a_history_guard()');
|
||||
DB::statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check');
|
||||
DB::statement("ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK ((permission IN ('company.view','company.edit','unit.create','access.manage') AND unit_id IS NULL) OR (permission IN ('unit.view','unit.edit') AND unit_id IS NOT NULL))");
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user