Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
-- Correção forward-only da compatibilidade entre a contratação canônica e a projeção legada.
-- tenant_modulo_contratado é a autoridade; nenhum estado legado concede acesso ao contrato.
create or replace function sigov.fn_tenant_modulo_compatibilizar() returns trigger
language plpgsql
security invoker
set search_path = pg_catalog, sigov
as $$
declare
v_modulo_id bigint;
v_habilitado boolean;
begin
select ms.id
into v_modulo_id
from sigov.modulo_saas ms
where ms.codigo = new.modulo_codigo
and ms.ativo
and not ms.is_deleted
limit 1;

if v_modulo_id is null then
return new;
end if;

-- A projeção só fica habilitada quando contrato, vigência e registro permitem.
-- SUSPENSO, INADIMPLENTE, CANCELADO e EXPIRADO continuam contratualmente
-- distinguíveis, mas nunca são convertidos em acesso habilitado.
v_habilitado := new.ativo
and new.status in ('TRIAL', 'EM_IMPLANTACAO', 'CONTRATADO', 'HABILITADO', 'ATIVO', 'BETA')
and (new.vigencia_inicio is null or new.vigencia_inicio <= current_date)
and (new.vigencia_fim is null or new.vigencia_fim >= current_date)
and (new.cancelamento_agendado_para is null or new.cancelamento_agendado_para > current_date);

insert into sigov.tenant_modulo (
tenant_id, modulo_saas_id, habilitado, contratado, inicio_at, fim_at,
configuracoes, ativo, created_by, updated_by, correlation_id)
values (
new.tenant_id, v_modulo_id, v_habilitado,
new.ativo and new.status not in ('DISPONIVEL', 'CANCELADO', 'EXPIRADO'),
coalesce(new.vigencia_inicio, current_date)::timestamptz,
new.vigencia_fim::timestamptz, new.parametros_json, new.ativo,
new.created_by, new.updated_by, new.correlation_id)
on conflict (tenant_id, modulo_saas_id) do update set
habilitado = excluded.habilitado,
contratado = excluded.contratado,
inicio_at = excluded.inicio_at,
fim_at = excluded.fim_at,
configuracoes = excluded.configuracoes,
ativo = excluded.ativo,
updated_at = now(),
updated_by = excluded.updated_by,
correlation_id = excluded.correlation_id;

return new;
end
$$;

-- Recriar corrige de forma determinística os cenários ausente, desabilitado,
-- associado a função divergente ou instalado com eventos/momento incorretos.
drop trigger if exists trg_tenant_modulo_compatibilizar on sigov.tenant_modulo_contratado;
create trigger trg_tenant_modulo_compatibilizar
after insert or update on sigov.tenant_modulo_contratado
for each row
execute function sigov.fn_tenant_modulo_compatibilizar();
Comment on lines +60 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Backfill legacy entitlements after replacing the trigger

For the installations this migration explicitly repairs because the old trigger was missing, disabled, or divergent, replacing the trigger does not process existing canonical rows. A contract already marked SUSPENSO/INADIMPLENTE, or with an elapsed scheduled cancellation, can therefore retain tenant_modulo.habilitado=true; legacy consumers such as TenantContextSwitchRepository and AgroModuleRepository continue granting access until an unrelated update fires the new trigger. Reproject the existing tenant_modulo_contratado rows as part of this migration.

AGENTS.md reference: AGENTS.md:L14-L14

Useful? React with 👍 / 👎.

55 changes: 53 additions & 2 deletions database/postgres/migrations/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2313,8 +2313,24 @@
"sql": "select case when not exists(select 1 from pg_trigger t join pg_proc p on p.oid=t.tgfoid join pg_namespace n on n.oid=p.pronamespace where t.tgrelid=to_regclass('sigov.tenant_modulo_contratado') and t.tgname='trg_tenant_modulo_contrato_auditar' and not t.tgisinternal and t.tgenabled<>'D' and n.nspname='sigov' and p.proname='fn_tenant_modulo_contrato_auditar' and pg_get_functiondef(p.oid) like '%coalesce(new.usuario_responsavel_id,new.updated_by,new.created_by)%') then 'objeto=trg_tenant_modulo_contrato_auditar esperado=habilitado com função corrigida obtido=ausente, desabilitado ou divergente' end"
},
{
"name": "trigger SaaS de compatibilidade corrigido",
"sql": "select case when not exists(select 1 from pg_trigger t join pg_proc p on p.oid=t.tgfoid join pg_namespace n on n.oid=p.pronamespace where t.tgrelid=to_regclass('sigov.tenant_modulo_contratado') and t.tgname='trg_tenant_modulo_compatibilizar' and not t.tgisinternal and t.tgenabled<>'D' and n.nspname='sigov' and p.proname='fn_tenant_modulo_compatibilizar' and pg_get_functiondef(p.oid) like '%case when new.ativo and new.status%') then 'objeto=trg_tenant_modulo_compatibilizar esperado=habilitado com suspensão preservada obtido=ausente, desabilitado ou divergente' end"
"name": "trigger SaaS existe na tabela canônica",
"sql": "select case when not exists(select 1 from pg_trigger t where t.tgrelid=to_regclass('sigov.tenant_modulo_contratado') and t.tgname='trg_tenant_modulo_compatibilizar' and not t.tgisinternal) then 'objeto=trg_tenant_modulo_compatibilizar esperado=existente em sigov.tenant_modulo_contratado obtido=ausente ou em outra tabela' end"
},
{
"name": "trigger SaaS habilitado para execução normal",
"sql": "select case when not exists(select 1 from pg_trigger t where t.tgrelid=to_regclass('sigov.tenant_modulo_contratado') and t.tgname='trg_tenant_modulo_compatibilizar' and not t.tgisinternal and t.tgenabled in ('O','A')) then 'objeto=trg_tenant_modulo_compatibilizar esperado=habilitado para execução normal obtido=desabilitado ou restrito a réplica' end"
},
{
"name": "trigger SaaS eventos momento e granularidade",
"sql": "select case when not exists(select 1 from pg_trigger t where t.tgrelid=to_regclass('sigov.tenant_modulo_contratado') and t.tgname='trg_tenant_modulo_compatibilizar' and not t.tgisinternal and (t.tgtype & 1)=1 and (t.tgtype & 2)=0 and (t.tgtype & 4)=4 and (t.tgtype & 16)=16 and (t.tgtype & 8)=0 and (t.tgtype & 32)=0 and t.tgqual is null) then 'objeto=trg_tenant_modulo_compatibilizar esperado=AFTER INSERT OR UPDATE FOR EACH ROW sem WHEN obtido=eventos, momento, granularidade ou condição divergente' end"
},
{
"name": "trigger SaaS função canônica",
"sql": "select case when not exists(select 1 from pg_trigger t join pg_proc p on p.oid=t.tgfoid join pg_namespace n on n.oid=p.pronamespace where t.tgrelid=to_regclass('sigov.tenant_modulo_contratado') and t.tgname='trg_tenant_modulo_compatibilizar' and not t.tgisinternal and n.nspname='sigov' and p.proname='fn_tenant_modulo_compatibilizar' and p.pronargs=0 and p.prorettype='trigger'::regtype) then 'objeto=trg_tenant_modulo_compatibilizar esperado=sigov.fn_tenant_modulo_compatibilizar() returns trigger obtido=função, schema ou assinatura divergente' end"
},
{
"name": "função SaaS contrato estrutural de projeção",
"sql": "select case when not exists(select 1 from pg_proc p join pg_namespace n on n.oid=p.pronamespace where n.nspname='sigov' and p.proname='fn_tenant_modulo_compatibilizar' and p.pronargs=0 and p.prorettype='trigger'::regtype and p.prosecdef=false and array_to_string(p.proconfig,',') like '%search_path=pg_catalog, sigov%') then 'objeto=sigov.fn_tenant_modulo_compatibilizar esperado=security invoker com search_path fixo obtido=privilégio ou resolução de schema divergente' end"
Comment on lines +2332 to +2333

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep correction-only probes off the earlier migration

On a fresh database, apply-migrations-manifest.ps1 executes every pending migration's probes immediately after that migration. When it reaches 20260910120000, the function created there has no fixed search_path, so this newly added p.proconfig probe fails and rolls back the transaction before 20260914130000 can install the definition being tested. Preserve the historical probe for 20260910120000 and validate this stricter contract only on the new corrective migration.

AGENTS.md reference: AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

}
]
},
Expand All @@ -2333,6 +2349,41 @@
"20260901090000"
],
"postConditionSql": "select exists(select 1 from information_schema.columns where table_schema='sigov' and table_name='manutencao_ordem_servico' and column_name='origem_tipo') and exists(select 1 from pg_index where indexrelid=to_regclass('sigov.ux_manutencao_os_origem') and indisvalid)"
},
{
"version": "20260914130000",
"description": "Correção da projeção SaaS com suspensão e vigência preservadas",
"category": "correction",
"file": "20260914130000_corr_saas_compatibilidade_suspensao.sql",
"checksum": "1b8b48920a5654b89b36aa167d294d8942e7f4c2f1c4774150419c9419f71a13",
"applyAutomatically": true,
"includeInBaseline": true,
"dependencies": [
"20260910120000"
],
"postConditionSql": "select exists(select 1 from pg_trigger t join pg_proc p on p.oid=t.tgfoid join pg_namespace n on n.oid=p.pronamespace where t.tgrelid=to_regclass('sigov.tenant_modulo_contratado') and t.tgname='trg_tenant_modulo_compatibilizar' and not t.tgisinternal and t.tgenabled in ('O','A') and n.nspname='sigov' and p.proname='fn_tenant_modulo_compatibilizar')",
"postConditionProbes": [
{
"name": "trigger SaaS existe na tabela canônica",
"sql": "select case when not exists(select 1 from pg_trigger t where t.tgrelid=to_regclass('sigov.tenant_modulo_contratado') and t.tgname='trg_tenant_modulo_compatibilizar' and not t.tgisinternal) then 'objeto=trg_tenant_modulo_compatibilizar esperado=existente em sigov.tenant_modulo_contratado obtido=ausente ou em outra tabela' end"
},
{
"name": "trigger SaaS habilitado para execução normal",
"sql": "select case when not exists(select 1 from pg_trigger t where t.tgrelid=to_regclass('sigov.tenant_modulo_contratado') and t.tgname='trg_tenant_modulo_compatibilizar' and not t.tgisinternal and t.tgenabled in ('O','A')) then 'objeto=trg_tenant_modulo_compatibilizar esperado=habilitado para execução normal obtido=desabilitado ou restrito a réplica' end"
},
{
"name": "trigger SaaS eventos momento e granularidade",
"sql": "select case when not exists(select 1 from pg_trigger t where t.tgrelid=to_regclass('sigov.tenant_modulo_contratado') and t.tgname='trg_tenant_modulo_compatibilizar' and not t.tgisinternal and (t.tgtype & 1)=1 and (t.tgtype & 2)=0 and (t.tgtype & 4)=4 and (t.tgtype & 16)=16 and (t.tgtype & 8)=0 and (t.tgtype & 32)=0 and t.tgqual is null) then 'objeto=trg_tenant_modulo_compatibilizar esperado=AFTER INSERT OR UPDATE FOR EACH ROW sem WHEN obtido=eventos, momento, granularidade ou condição divergente' end"
},
{
"name": "trigger SaaS função canônica",
"sql": "select case when not exists(select 1 from pg_trigger t join pg_proc p on p.oid=t.tgfoid join pg_namespace n on n.oid=p.pronamespace where t.tgrelid=to_regclass('sigov.tenant_modulo_contratado') and t.tgname='trg_tenant_modulo_compatibilizar' and not t.tgisinternal and n.nspname='sigov' and p.proname='fn_tenant_modulo_compatibilizar' and p.pronargs=0 and p.prorettype='trigger'::regtype) then 'objeto=trg_tenant_modulo_compatibilizar esperado=sigov.fn_tenant_modulo_compatibilizar() returns trigger obtido=função, schema ou assinatura divergente' end"
},
{
"name": "função SaaS contrato estrutural de projeção",
"sql": "select case when not exists(select 1 from pg_proc p join pg_namespace n on n.oid=p.pronamespace where n.nspname='sigov' and p.proname='fn_tenant_modulo_compatibilizar' and p.pronargs=0 and p.prorettype='trigger'::regtype and p.prosecdef=false and array_to_string(p.proconfig,',') like '%search_path=pg_catalog, sigov%') then 'objeto=sigov.fn_tenant_modulo_compatibilizar esperado=security invoker com search_path fixo obtido=privilégio ou resolução de schema divergente' end"
}
]
}
],
"compatibilityAfterAll": [
Expand Down
80 changes: 79 additions & 1 deletion database/postgres/script_completo.sql
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,9 @@ select exists (
('20260903230000', array['2d132eb414ccd2352b302a6d50206f735f2995cc73f02e10bc6eacb3991f0f70']::text[]),
('20260908120000', array['c7e27f2942419883e6b7123c5ed16e0b574c520a6deabe4ce71b4f375ba272e7']::text[]),
('20260909120000', array['0dbe94d680f16fd4bb2d50c5215a96fab00a4d70278f28099eaa6d078d7df52e']::text[]),
('20260910120000', array['f240636bb20ca9b890162f3ab61e00b82b45d55197ee539c537ef766cfa8acee']::text[])
('20260910120000', array['f240636bb20ca9b890162f3ab61e00b82b45d55197ee539c537ef766cfa8acee']::text[]),
('20260914120000', array['465024809f7f4d900e442e60b22857068b497778739019c1c735f67b8488733d','0bbefaf91b47151dc8aa839888c9d3cb7d2dcb6903649553740c3b6b1f02cd02']::text[]),
('20260914130000', array['1b8b48920a5654b89b36aa167d294d8942e7f4c2f1c4774150419c9419f71a13']::text[])
) required(version, accepted_checksums)
left join sigov.schema_migrations applied on applied.version = required.version
where applied.version is null
Expand Down Expand Up @@ -30786,6 +30788,82 @@ drop function if exists pg_temp.create_index_when_columns_exist(text,text,text,t
drop function if exists pg_temp.create_index_when_columns_exist(text,text,text,text[],text,text);
drop function if exists pg_temp.ensure_schema_safe_index(text,text,text,text[],text);

-- ==================================================
-- MIGRATION: 20260914130000_corr_saas_compatibilidade_suspensao.sql
-- CATEGORY: correction
-- CHECKSUM_SHA256: 1b8b48920a5654b89b36aa167d294d8942e7f4c2f1c4774150419c9419f71a13
-- ==================================================
-- Correção forward-only da compatibilidade entre a contratação canônica e a projeção legada.
-- tenant_modulo_contratado é a autoridade; nenhum estado legado concede acesso ao contrato.
create or replace function sigov.fn_tenant_modulo_compatibilizar() returns trigger
language plpgsql
security invoker
set search_path = pg_catalog, sigov
as $$
declare
v_modulo_id bigint;
v_habilitado boolean;
begin
select ms.id
into v_modulo_id
from sigov.modulo_saas ms
where ms.codigo = new.modulo_codigo
and ms.ativo
and not ms.is_deleted
limit 1;

if v_modulo_id is null then
return new;
end if;

-- A projeção só fica habilitada quando contrato, vigência e registro permitem.
-- SUSPENSO, INADIMPLENTE, CANCELADO e EXPIRADO continuam contratualmente
-- distinguíveis, mas nunca são convertidos em acesso habilitado.
v_habilitado := new.ativo
and new.status in ('TRIAL', 'EM_IMPLANTACAO', 'CONTRATADO', 'HABILITADO', 'ATIVO', 'BETA')
and (new.vigencia_inicio is null or new.vigencia_inicio <= current_date)
and (new.vigencia_fim is null or new.vigencia_fim >= current_date)
and (new.cancelamento_agendado_para is null or new.cancelamento_agendado_para > current_date);

insert into sigov.tenant_modulo (
tenant_id, modulo_saas_id, habilitado, contratado, inicio_at, fim_at,
configuracoes, ativo, created_by, updated_by, correlation_id)
values (
new.tenant_id, v_modulo_id, v_habilitado,
new.ativo and new.status not in ('DISPONIVEL', 'CANCELADO', 'EXPIRADO'),
coalesce(new.vigencia_inicio, current_date)::timestamptz,
new.vigencia_fim::timestamptz, new.parametros_json, new.ativo,
new.created_by, new.updated_by, new.correlation_id)
on conflict (tenant_id, modulo_saas_id) do update set
habilitado = excluded.habilitado,
contratado = excluded.contratado,
inicio_at = excluded.inicio_at,
fim_at = excluded.fim_at,
configuracoes = excluded.configuracoes,
ativo = excluded.ativo,
updated_at = now(),
updated_by = excluded.updated_by,
correlation_id = excluded.correlation_id;

return new;
end
$$;

-- Recriar corrige de forma determinística os cenários ausente, desabilitado,
-- associado a função divergente ou instalado com eventos/momento incorretos.
drop trigger if exists trg_tenant_modulo_compatibilizar on sigov.tenant_modulo_contratado;
create trigger trg_tenant_modulo_compatibilizar
after insert or update on sigov.tenant_modulo_contratado
for each row
execute function sigov.fn_tenant_modulo_compatibilizar();

insert into sigov.schema_migrations(version, description, checksum, category, source, success, execution_ms, applied_at) values ('20260914130000', 'Correção da projeção SaaS com suspensão e vigência preservadas', '1b8b48920a5654b89b36aa167d294d8942e7f4c2f1c4774150419c9419f71a13', 'correction', 'script_completop', true, null, now()) on conflict (version) do update set description = excluded.description, checksum = excluded.checksum, category = excluded.category, source = excluded.source, success = true;

-- Reset de helpers temporários entre migrations concatenadas.
drop function if exists pg_temp.create_index_when_columns_exist(text,text,text,text[],text);
drop function if exists pg_temp.create_index_when_columns_exist(text,text,text,text[],text,text);
drop function if exists pg_temp.ensure_schema_safe_index(text,text,text,text[],text);

-- ==================================================
-- COMPATIBILITY: 850_post_migration_compatibility.sql
-- STAGE: AFTER ALL MIGRATIONS
Expand Down
Loading
Loading