Preserva classificacao contabilistica aceite e bases locais
Regista aceitacao funcional do primeiro incremento PGC-NIRF e preserva as dependencias documentais e tecnicas existentes. A validacao do calculo fiscal continua separada e pendente. Sem credenciais, bases operacionais ou publicacao.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
<?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('doc_records', function (Blueprint $table): void {
|
||||
$table->id(); $table->unsignedBigInteger('tenant_id'); $table->unsignedBigInteger('company_id'); $table->unsignedBigInteger('unit_id')->nullable();
|
||||
$table->string('kind', 30); $table->unsignedBigInteger('record_id'); $table->unsignedInteger('version'); $table->jsonb('data'); $table->timestampsTz();
|
||||
$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();
|
||||
});
|
||||
DB::statement("CREATE UNIQUE INDEX doc_record_identity ON doc_records(company_id,kind,record_id) WHERE kind <> 'payment'");
|
||||
DB::statement("ALTER TABLE doc_records ADD CHECK (version > 0 AND jsonb_typeof(data) = 'object' AND ((kind IN ('issuer','client','payment') AND unit_id IS NULL) OR (kind IN ('service_drafts','billing_drafts') AND unit_id IS NOT NULL)))");
|
||||
Schema::create('doc_history', function (Blueprint $table): void {
|
||||
$table->id(); $table->foreignId('doc_record_id')->constrained('doc_records')->restrictOnDelete(); $table->unsignedInteger('version');
|
||||
$table->foreignId('actor_id')->constrained('users')->restrictOnDelete(); $table->jsonb('snapshot'); $table->timestampTz('created_at');
|
||||
$table->unique(['doc_record_id','version']);
|
||||
});
|
||||
DB::unprepared(<<<'SQL'
|
||||
CREATE OR REPLACE FUNCTION sgf_doc_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE item record; target text; instruction jsonb;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN RAISE EXCEPTION 'SGF_DOC_NO_DELETE'; END IF;
|
||||
IF TG_OP = 'INSERT' AND NEW.version <> 1 THEN RAISE EXCEPTION 'SGF_DOC_INITIAL_VERSION'; END IF;
|
||||
IF TG_OP = 'UPDATE' THEN
|
||||
IF ROW(NEW.id,NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.kind,NEW.record_id) IS DISTINCT FROM ROW(OLD.id,OLD.tenant_id,OLD.company_id,OLD.unit_id,OLD.kind,OLD.record_id) THEN RAISE EXCEPTION 'SGF_DOC_IMMUTABLE_SCOPE'; END IF;
|
||||
IF NEW.version <> OLD.version + 1 THEN RAISE EXCEPTION 'SGF_DOC_VERSION'; END IF;
|
||||
END IF;
|
||||
IF NEW.kind = 'issuer' AND NEW.record_id <> NEW.company_id THEN RAISE EXCEPTION 'SGF_DOC_ISSUER'; END IF;
|
||||
IF NEW.kind = 'client' AND NOT EXISTS (SELECT 1 FROM a_clients WHERE id=NEW.record_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id) THEN RAISE EXCEPTION 'SGF_DOC_CLIENT'; END IF;
|
||||
IF NEW.kind = 'payment' AND NEW.record_id <> 0 THEN RAISE EXCEPTION 'SGF_DOC_PAYMENT'; END IF;
|
||||
IF NEW.kind IN ('service_drafts','billing_drafts') THEN
|
||||
target := 'a_' || NEW.kind;
|
||||
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 ROW(item.tenant_id,item.company_id,item.unit_id) IS DISTINCT FROM ROW(NEW.tenant_id,NEW.company_id,NEW.unit_id) THEN RAISE EXCEPTION 'SGF_DOC_DRAFT_SCOPE'; END IF;
|
||||
IF NEW.kind = 'billing_drafts' AND EXISTS (SELECT 1 FROM b_internal_confirmations WHERE billing_draft_id=NEW.record_id) THEN RAISE EXCEPTION 'SGF_DOC_CONFIRMED_IMMUTABLE'; END IF;
|
||||
IF (NEW.data->>'draft_version')::integer IS DISTINCT FROM item.version THEN RAISE EXCEPTION 'SGF_DOC_STALE_DRAFT'; END IF;
|
||||
IF NEW.data->>'price_mode' NOT IN ('undefined','included','added') THEN RAISE EXCEPTION 'SGF_DOC_PRICE_MODE'; END IF;
|
||||
FOR instruction IN SELECT * FROM jsonb_array_elements(NEW.data->'payment_instruction_ids') LOOP
|
||||
IF NOT EXISTS (SELECT 1 FROM doc_records WHERE id=instruction::text::bigint AND kind='payment' AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id) THEN RAISE EXCEPTION 'SGF_DOC_FOREIGN_PAYMENT'; END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END; $$;
|
||||
CREATE OR REPLACE FUNCTION sgf_doc_history_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM doc_records WHERE id=NEW.doc_record_id AND version=NEW.version AND data=NEW.snapshot) THEN RAISE EXCEPTION 'SGF_DOC_HISTORY_VERSION'; END IF;
|
||||
RETURN NEW;
|
||||
END; $$;
|
||||
CREATE TRIGGER doc_record_guard BEFORE INSERT OR UPDATE OR DELETE ON doc_records FOR EACH ROW EXECUTE FUNCTION sgf_doc_guard();
|
||||
CREATE TRIGGER doc_history_guard BEFORE INSERT ON doc_history FOR EACH ROW EXECUTE FUNCTION sgf_doc_history_guard();
|
||||
CREATE TRIGGER doc_history_immutable BEFORE UPDATE OR DELETE ON doc_history FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
|
||||
SQL);
|
||||
$company = ['company.view','company.edit','unit.create','access.manage','issuer_documents.view','issuer_documents.edit','client_documents.view','client_documents.edit'];
|
||||
$unit = ['unit.view','unit.edit','billing_drafts.confirm_internal','draft_documents.view','draft_documents.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 { throw new RuntimeException('Document preparation contains historical data; use a separately reviewed preservation procedure.'); }
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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('document_preview_models', function (Blueprint $table): void {
|
||||
$table->id(); $table->unsignedBigInteger('tenant_id'); $table->unsignedBigInteger('company_id')->unique();
|
||||
$table->unsignedInteger('version'); $table->jsonb('data'); $table->timestampsTz();
|
||||
$table->foreign(['tenant_id','company_id'])->references(['tenant_id','id'])->on('companies')->restrictOnDelete();
|
||||
});
|
||||
Schema::create('document_preview_model_history', function (Blueprint $table): void {
|
||||
$table->id(); $table->foreignId('model_id')->constrained('document_preview_models')->restrictOnDelete();
|
||||
$table->unsignedInteger('version'); $table->foreignId('actor_id')->constrained('users')->restrictOnDelete();
|
||||
$table->jsonb('snapshot'); $table->timestampTz('created_at'); $table->unique(['model_id','version']);
|
||||
});
|
||||
DB::unprepared(<<<'SQL'
|
||||
CREATE OR REPLACE FUNCTION sgf_preview_model_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN RAISE EXCEPTION 'SGF_PREVIEW_NO_DELETE'; END IF;
|
||||
IF TG_OP = 'INSERT' AND NEW.version <> 1 THEN RAISE EXCEPTION 'SGF_PREVIEW_INITIAL_VERSION'; END IF;
|
||||
IF TG_OP = 'UPDATE' AND (ROW(NEW.id,NEW.tenant_id,NEW.company_id) IS DISTINCT FROM ROW(OLD.id,OLD.tenant_id,OLD.company_id) OR NEW.version <> OLD.version+1) THEN RAISE EXCEPTION 'SGF_PREVIEW_IMMUTABLE_SCOPE_OR_VERSION'; END IF;
|
||||
IF jsonb_typeof(NEW.data) IS DISTINCT FROM 'object' OR (NEW.data - ARRAY['show_logo','show_contacts','show_observations','show_payment_instructions','contact_text','observations']) <> '{}'::jsonb THEN RAISE EXCEPTION 'SGF_PREVIEW_FIXED_CONTENT'; END IF;
|
||||
IF jsonb_typeof(NEW.data->'show_logo') IS DISTINCT FROM 'boolean' OR jsonb_typeof(NEW.data->'show_contacts') IS DISTINCT FROM 'boolean' OR jsonb_typeof(NEW.data->'show_observations') IS DISTINCT FROM 'boolean' OR jsonb_typeof(NEW.data->'show_payment_instructions') IS DISTINCT FROM 'boolean' THEN RAISE EXCEPTION 'SGF_PREVIEW_BOOLEAN'; END IF;
|
||||
RETURN NEW;
|
||||
END; $$;
|
||||
CREATE OR REPLACE FUNCTION sgf_preview_history_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM document_preview_models WHERE id=NEW.model_id AND version=NEW.version AND data=NEW.snapshot) THEN RAISE EXCEPTION 'SGF_PREVIEW_HISTORY'; END IF;
|
||||
RETURN NEW;
|
||||
END; $$;
|
||||
CREATE TRIGGER preview_model_guard BEFORE INSERT OR UPDATE OR DELETE ON document_preview_models FOR EACH ROW EXECUTE FUNCTION sgf_preview_model_guard();
|
||||
CREATE TRIGGER preview_model_history_guard BEFORE INSERT ON document_preview_model_history FOR EACH ROW EXECUTE FUNCTION sgf_preview_history_guard();
|
||||
CREATE TRIGGER preview_model_history_immutable BEFORE UPDATE OR DELETE ON document_preview_model_history FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
|
||||
SQL);
|
||||
$company = ['company.view','company.edit','unit.create','access.manage','issuer_documents.view','issuer_documents.edit','client_documents.view','client_documents.edit','document_preview.configure'];
|
||||
$unit = ['unit.view','unit.edit','billing_drafts.confirm_internal','draft_documents.view','draft_documents.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 { throw new RuntimeException('Preview model history requires a separately reviewed preservation procedure.'); }
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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('service_calculation_profiles', function (Blueprint $t): void {
|
||||
$t->id(); $t->unsignedBigInteger('tenant_id'); $t->unsignedBigInteger('company_id'); $t->unsignedBigInteger('unit_id');
|
||||
$t->string('identifier'); $t->unsignedInteger('version'); $t->jsonb('definition'); $t->timestampTz('created_at');
|
||||
$t->unique(['unit_id','identifier','version']);
|
||||
$t->foreign(['tenant_id','company_id','unit_id'])->references(['tenant_id','company_id','id'])->on('units')->restrictOnDelete();
|
||||
});
|
||||
Schema::create('service_calculation_results', function (Blueprint $t): void {
|
||||
$t->id(); $t->unsignedBigInteger('tenant_id'); $t->unsignedBigInteger('company_id'); $t->unsignedBigInteger('unit_id');
|
||||
$t->foreignId('billing_draft_id')->constrained('a_billing_drafts')->restrictOnDelete();
|
||||
$t->foreignId('profile_id')->constrained('service_calculation_profiles')->restrictOnDelete();
|
||||
$t->foreignId('actor_id')->constrained('users')->restrictOnDelete();
|
||||
$t->string('fingerprint', 64); $t->jsonb('snapshot'); $t->timestampTz('created_at');
|
||||
$t->unique(['billing_draft_id','fingerprint']);
|
||||
$t->foreign(['tenant_id','company_id','unit_id'])->references(['tenant_id','company_id','id'])->on('units')->restrictOnDelete();
|
||||
});
|
||||
DB::unprepared(<<<'SQL'
|
||||
CREATE OR REPLACE FUNCTION sgf_calculation_insert_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NOT EXISTS(SELECT 1 FROM a_billing_drafts b WHERE b.id=NEW.billing_draft_id AND b.tenant_id=NEW.tenant_id AND b.company_id=NEW.company_id AND b.unit_id=NEW.unit_id)
|
||||
OR NOT EXISTS(SELECT 1 FROM service_calculation_profiles p WHERE p.id=NEW.profile_id AND p.tenant_id=NEW.tenant_id AND p.company_id=NEW.company_id AND p.unit_id=NEW.unit_id)
|
||||
THEN RAISE EXCEPTION 'SGF_CALCULATION_SCOPE'; END IF;
|
||||
RETURN NEW;
|
||||
END; $$;
|
||||
CREATE TRIGGER calculation_scope BEFORE INSERT ON service_calculation_results FOR EACH ROW EXECUTE FUNCTION sgf_calculation_insert_guard();
|
||||
CREATE TRIGGER calculation_results_immutable BEFORE UPDATE OR DELETE ON service_calculation_results FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
|
||||
CREATE TRIGGER calculation_profiles_immutable BEFORE UPDATE OR DELETE ON service_calculation_profiles FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();
|
||||
SQL);
|
||||
}
|
||||
public function down(): void { throw new RuntimeException('Calculation history requires a reviewed preservation procedure.'); }
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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('ca_proposals', function (Blueprint $t): void {
|
||||
$t->id(); $t->unsignedBigInteger('tenant_id'); $t->unsignedBigInteger('company_id');
|
||||
$t->string('kind',30); $t->string('entity_key',100); $t->unsignedInteger('version');
|
||||
$t->foreignId('previous_id')->nullable()->constrained('ca_proposals')->restrictOnDelete();
|
||||
$t->foreignId('replaces_id')->nullable()->constrained('ca_proposals')->restrictOnDelete();
|
||||
$t->date('valid_from'); $t->date('valid_until')->nullable(); $t->jsonb('data');
|
||||
$t->text('reason'); $t->foreignId('actor_id')->constrained('users')->restrictOnDelete(); $t->timestampTz('created_at');
|
||||
$t->unique(['company_id','kind','entity_key','version']); $t->unique(['tenant_id','company_id','id']);
|
||||
$t->foreign(['tenant_id','company_id'])->references(['tenant_id','id'])->on('companies')->restrictOnDelete();
|
||||
});
|
||||
foreach (['ca_submissions','ca_decisions'] as $table) {
|
||||
Schema::create($table,function (Blueprint $t) use ($table): void {
|
||||
$t->id(); $t->unsignedBigInteger('tenant_id'); $t->unsignedBigInteger('company_id'); $t->unsignedBigInteger('proposal_id')->unique();
|
||||
$t->foreignId('actor_id')->constrained('users')->restrictOnDelete(); $t->timestampTz('created_at');
|
||||
if ($table==='ca_decisions') { $t->string('decision',15); $t->text('reason'); }
|
||||
$t->foreign(['tenant_id','company_id','proposal_id'])->references(['tenant_id','company_id','id'])->on('ca_proposals')->restrictOnDelete();
|
||||
});
|
||||
}
|
||||
Schema::create('ca_resolutions',function (Blueprint $t): void {
|
||||
$t->id(); $t->unsignedBigInteger('tenant_id'); $t->unsignedBigInteger('company_id'); $t->unsignedBigInteger('unit_id');
|
||||
$t->string('area',30); $t->unsignedBigInteger('record_id'); $t->unsignedInteger('record_version'); $t->unsignedBigInteger('service_id');
|
||||
$t->date('reference_date'); $t->jsonb('snapshot'); $t->foreignId('actor_id')->constrained('users')->restrictOnDelete(); $t->timestampTz('created_at');
|
||||
$t->foreign(['tenant_id','company_id','unit_id'])->references(['tenant_id','company_id','id'])->on('units')->restrictOnDelete();
|
||||
$t->foreign(['tenant_id','company_id','service_id'])->references(['tenant_id','company_id','id'])->on('a_services')->restrictOnDelete();
|
||||
});
|
||||
Schema::create('ca_commands',function (Blueprint $t): void {
|
||||
$t->id(); $t->unsignedBigInteger('tenant_id'); $t->unsignedBigInteger('company_id'); $t->foreignId('actor_id')->constrained('users')->restrictOnDelete();
|
||||
$t->string('command_key',100); $t->string('request_hash',64); $t->jsonb('result'); $t->timestampTz('created_at');
|
||||
$t->unique(['company_id','command_key']);
|
||||
$t->foreign(['tenant_id','company_id'])->references(['tenant_id','id'])->on('companies')->restrictOnDelete();
|
||||
});
|
||||
DB::unprepared(<<<'SQL'
|
||||
ALTER TABLE ca_proposals ADD CHECK (kind IN ('plan','account','category','service_link','service_override'));
|
||||
ALTER TABLE ca_proposals ADD CHECK (valid_until IS NULL OR valid_until >= valid_from);
|
||||
ALTER TABLE ca_decisions ADD CHECK (decision IN ('approved','refused'));
|
||||
CREATE OR REPLACE FUNCTION sgf_ca_proposal_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE r record; ref text; keys text[];
|
||||
BEGIN
|
||||
IF NEW.previous_id IS NULL THEN
|
||||
IF NEW.version<>1 THEN RAISE EXCEPTION 'SGF_CA_INITIAL_VERSION'; END IF;
|
||||
ELSE
|
||||
SELECT * INTO r FROM ca_proposals WHERE id=NEW.previous_id;
|
||||
IF NOT FOUND OR ROW(r.tenant_id,r.company_id,r.kind,r.entity_key,r.version+1) IS DISTINCT FROM ROW(NEW.tenant_id,NEW.company_id,NEW.kind,NEW.entity_key,NEW.version) THEN RAISE EXCEPTION 'SGF_CA_PREVIOUS_SCOPE'; END IF;
|
||||
END IF;
|
||||
IF NEW.replaces_id IS NOT NULL AND NOT EXISTS(SELECT 1 FROM ca_proposals p JOIN ca_decisions d ON d.proposal_id=p.id AND d.decision='approved' WHERE p.id=NEW.replaces_id AND p.tenant_id=NEW.tenant_id AND p.company_id=NEW.company_id AND p.kind=NEW.kind AND p.entity_key=NEW.entity_key AND p.valid_from<=NEW.valid_from) THEN RAISE EXCEPTION 'SGF_CA_REPLACEMENT_SCOPE'; END IF;
|
||||
keys:=CASE NEW.kind WHEN 'plan' THEN ARRAY['plan','source_version'] WHEN 'account' THEN ARRAY['plan_id','code','name','origin','movable','evidence'] WHEN 'category' THEN ARRAY['name','account_id','conditions'] WHEN 'service_link' THEN ARRAY['service_id','category_id'] ELSE ARRAY['service_id','account_id','conditions'] END;
|
||||
IF jsonb_typeof(NEW.data) IS DISTINCT FROM 'object' OR NEW.data-keys<>'{}'::jsonb THEN RAISE EXCEPTION 'SGF_CA_UNSUPPORTED_DATA'; END IF;
|
||||
FOREACH ref IN ARRAY ARRAY['plan_id','account_id','category_id'] LOOP
|
||||
IF NEW.data ? ref AND NOT EXISTS(SELECT 1 FROM ca_proposals p WHERE p.id=(NEW.data->>ref)::bigint AND p.tenant_id=NEW.tenant_id AND p.company_id=NEW.company_id AND p.kind=CASE ref WHEN 'plan_id' THEN 'plan' WHEN 'account_id' THEN 'account' ELSE 'category' END) THEN RAISE EXCEPTION 'SGF_CA_REFERENCE_SCOPE'; END IF;
|
||||
END LOOP;
|
||||
IF NEW.data ? 'service_id' AND NOT EXISTS(SELECT 1 FROM a_services s WHERE s.id=(NEW.data->>'service_id')::bigint AND s.tenant_id=NEW.tenant_id AND s.company_id=NEW.company_id) THEN RAISE EXCEPTION 'SGF_CA_SERVICE_SCOPE'; END IF;
|
||||
IF NEW.kind IN ('service_link','service_override') AND NEW.entity_key<>'service:'||(NEW.data->>'service_id') THEN RAISE EXCEPTION 'SGF_CA_SERVICE_KEY'; END IF;
|
||||
IF NEW.kind='plan' AND (NEW.entity_key<>'PGC-NIRF' OR NEW.data->>'plan'<>'PGC-NIRF') THEN RAISE EXCEPTION 'SGF_CA_PLAN'; END IF;
|
||||
RETURN NEW;
|
||||
END; $$;
|
||||
CREATE TRIGGER ca_proposal_insert BEFORE INSERT ON ca_proposals FOR EACH ROW EXECUTE FUNCTION sgf_ca_proposal_guard();
|
||||
CREATE OR REPLACE FUNCTION sgf_ca_event_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE p record;
|
||||
BEGIN
|
||||
SELECT * INTO p FROM ca_proposals WHERE id=NEW.proposal_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id;
|
||||
IF NOT FOUND OR EXISTS(SELECT 1 FROM ca_proposals n WHERE n.company_id=p.company_id AND n.kind=p.kind AND n.entity_key=p.entity_key AND n.version>p.version) THEN RAISE EXCEPTION 'SGF_CA_EVENT_VERSION'; END IF;
|
||||
IF TG_TABLE_NAME='ca_decisions' AND NOT EXISTS(SELECT 1 FROM ca_submissions WHERE proposal_id=p.id) THEN RAISE EXCEPTION 'SGF_CA_NOT_SUBMITTED'; END IF;
|
||||
RETURN NEW;
|
||||
END; $$;
|
||||
CREATE TRIGGER ca_submission_insert BEFORE INSERT ON ca_submissions FOR EACH ROW EXECUTE FUNCTION sgf_ca_event_guard();
|
||||
CREATE TRIGGER ca_decision_insert BEFORE INSERT ON ca_decisions FOR EACH ROW EXECUTE FUNCTION sgf_ca_event_guard();
|
||||
CREATE OR REPLACE FUNCTION sgf_ca_resolution_guard() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.area='service_drafts' THEN
|
||||
IF NOT EXISTS(SELECT 1 FROM a_service_drafts r WHERE r.id=NEW.record_id AND r.tenant_id=NEW.tenant_id AND r.company_id=NEW.company_id AND r.unit_id=NEW.unit_id AND r.version=NEW.record_version AND EXISTS(SELECT 1 FROM jsonb_array_elements(r.data->'lines') l WHERE (l->>'service_id')::bigint=NEW.service_id)) THEN RAISE EXCEPTION 'SGF_CA_OPERATION_SCOPE'; END IF;
|
||||
ELSIF NEW.area='billing_drafts' THEN
|
||||
IF NOT EXISTS(SELECT 1 FROM a_billing_drafts r WHERE r.id=NEW.record_id AND r.tenant_id=NEW.tenant_id AND r.company_id=NEW.company_id AND r.unit_id=NEW.unit_id AND r.version=NEW.record_version AND EXISTS(SELECT 1 FROM jsonb_array_elements(r.data->'lines') l WHERE (l->>'service_id')::bigint=NEW.service_id)) THEN RAISE EXCEPTION 'SGF_CA_OPERATION_SCOPE'; END IF;
|
||||
ELSE RAISE EXCEPTION 'SGF_CA_OPERATION_KIND'; END IF;
|
||||
RETURN NEW;
|
||||
END; $$;
|
||||
CREATE TRIGGER ca_resolution_insert BEFORE INSERT ON ca_resolutions FOR EACH ROW EXECUTE FUNCTION sgf_ca_resolution_guard();
|
||||
SQL);
|
||||
foreach (['ca_proposals','ca_submissions','ca_decisions','ca_resolutions','ca_commands'] as $table) { DB::statement("CREATE TRIGGER {$table}_immutable BEFORE UPDATE OR DELETE ON $table FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()"); }
|
||||
$company=['company.view','company.edit','unit.create','access.manage','issuer_documents.view','issuer_documents.edit','client_documents.view','client_documents.edit','document_preview.configure','catalogue_accounting.view','catalogue_accounting.propose','catalogue_accounting.approve'];
|
||||
$unit=['unit.view','unit.edit','billing_drafts.confirm_internal','draft_documents.view','draft_documents.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"; } } }
|
||||
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 ('".implode("','",$company)."') AND unit_id IS NULL) OR (permission IN ('".implode("','",$unit)."') AND unit_id IS NOT NULL))");
|
||||
}
|
||||
public function down(): void { throw new RuntimeException('Accounting classification history requires a preservation procedure.'); }
|
||||
};
|
||||
Reference in New Issue
Block a user