id(); $table->unsignedBigInteger('tenant_id'); $table->unsignedBigInteger('company_id'); $table->unsignedBigInteger('unit_id'); $table->foreignId('billing_draft_id')->unique()->constrained('a_billing_drafts')->restrictOnDelete(); $table->unsignedInteger('billing_version'); $table->unsignedInteger('version')->default(1); $table->foreignId('actor_id')->constrained('users')->restrictOnDelete(); $table->jsonb('snapshot'); $table->timestampTz('created_at'); $table->foreign(['tenant_id','company_id','unit_id','billing_draft_id'])->references(['tenant_id','company_id','unit_id','id'])->on('a_billing_drafts')->restrictOnDelete(); }); DB::statement('ALTER TABLE b_internal_confirmations ADD CHECK (version = 1 AND billing_version > 0 AND jsonb_typeof(snapshot) = \'object\')'); DB::unprepared(<<<'SQL' CREATE OR REPLACE FUNCTION sgf_b_confirmation_guard() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN PERFORM 1 FROM a_billing_drafts WHERE id = NEW.billing_draft_id AND version = NEW.billing_version FOR UPDATE; IF NOT FOUND THEN RAISE EXCEPTION 'SGF_B_STALE_DRAFT'; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION sgf_b_draft_locked() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF EXISTS (SELECT 1 FROM b_internal_confirmations WHERE billing_draft_id = OLD.id) THEN RAISE EXCEPTION 'SGF_B_CONFIRMED_IMMUTABLE'; END IF; IF TG_OP = 'DELETE' THEN RETURN OLD; 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' WHEN 'internal_confirmations' THEN 'b_internal_confirmations' 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; $$; CREATE TRIGGER b_confirmation_valid BEFORE INSERT ON b_internal_confirmations FOR EACH ROW EXECUTE FUNCTION sgf_b_confirmation_guard(); CREATE TRIGGER b_confirmation_immutable BEFORE UPDATE OR DELETE ON b_internal_confirmations FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); CREATE TRIGGER b_billing_locked BEFORE UPDATE OR DELETE ON a_billing_drafts FOR EACH ROW EXECUTE FUNCTION sgf_b_draft_locked(); SQL); $company = ['company.view','company.edit','unit.create','access.manage']; $unit = ['unit.view','unit.edit','billing_drafts.confirm_internal']; 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('Internal confirmations are immutable; rollback requires a separately reviewed preservation procedure.'); } };