+
    qj:                     4  a  R! t 0 t R t^ RIt^ RIt^ RIHt ^ RIHt ^ RIH	t	 ^ RI
Ht ]P                  ! ]4      t^tRt^(tR"tR R ltR#R	 R
 lltR0tR R ltR R ltRtRs] ^ k R R ltR^RRRR/R R lltR R ltR$R R lltR%R R lltR R  ltR# )&u  Leitura do grafo: vizinhança de um NIF, ficha de entidade, cobertura.

A travessia é BFS por níveis em Python e não um CTE recursivo. O CTE fica
documentado no `registry_graph` como referência, mas para servir a UI o BFS ganha
em três coisas que importam aqui: dá a profundidade mínima exacta (um CTE
re-expande um nó alcançado por dois caminhos), permite tectos por nível em vez de
um tecto global, e é instrumentável — quando um grafo vem truncado é possível
dizer *onde* foi cortado.

Três tectos, e nenhum deles é decorativo:

* **profundidade** — 3 hops. Cada hop multiplica; ao quarto o grafo deixa de se
  ler.
* **grau do nó** — um gerente nomeado em 400 empresas, um banco como sócio, uma
  cooperativa com 300 associados. Atravessá-los liga tudo a tudo e o resultado
  deixa de significar nada. Não se escondem: aparecem como nó, marcados, mas não
  se expandem.
* **número de nós** — e quando se bate no tecto diz-se `truncated: true`, em vez
  de devolver metade do grafo com ar de estar completo.
N)Any)text)AsyncSession)normalize_namei  c                L    V ^8  d   QhR\         R,          R\         R,          /# )   rawNreturn)str)formats   "app/services/registry_query.py__annotate__r   +   s"     0 03: 0#* 0    c                 n    V '       g   R # RP                  R V  4       4      p\        V4      ^	8X  d   V# R # )N c              3   T   "   T F  qP                  4       '       g   K  Vx  K   	  R # 5iN)isdigit).0chs   & r   	<genexpr>_norm_nif.<locals>.<genexpr>.   s     6#BRR#s   (
()joinlen)r   digitss   & r   	_norm_nifr   +   s2    WW6#66F[A%6/4/r   c                    V ^8  d   QhR\         R\        \        ,          R\        R,          R\        \        \        \        \
        3,          3,          /# )r   sessionkeyslimitNr	   )r   listr
   intdictr   )r   s   "r   r   r   2   sL     D DD!%cD36:D	#tCH~
Dr   c                  "   V'       g   / # V'       d   RMRpT P                  \        RV,           4      RV/V'       d   RV/M/ C4      G Rj  xL
 P                  4       P                  4       p/ pV FL  p\	        V4      p\        VP                  R4      VP                  R4      4      w  VR	&   VR
&   WuVR,          &   KN  	  V#  Lv5i)u/  Nós indexados pela `entity_key`, não pelo NIF.

Um titular estrangeiro ou não identificado não tem NIF — são 67 arestas
actuais em 29 empresas. Indexados por NIF ficavam de fora do dicionário de
nós, a aresta chegava ao frontend a apontar para o nada e era descartada em
silêncio: a ficha listava o sócio e o diagrama não o desenhava, sem nada
dizer que faltava ali alguém.

A `entity_key` existe para todas as entidades — para as que têm NIF é
derivada dele —, portanto passa a ser a chave da travessia.

O `limit` traz a ordenação por relevância para dentro da consulta. A
alternativa — trazer tudo e cortar em Python — obrigava a ler cinco mil
linhas para ficar com quatrocentas, e mede-se: eram 400 ms a três hops.
Monitorizadas primeiro, depois insolvências abertas, depois o grau.
z
        ORDER BY (s.monitored_company_id IS NOT NULL) DESC,
                 COALESCE(s.insolvency_open, FALSE) DESC,
                 COALESCE(s.cire_ann_as_debtor, 0) DESC,
                 e.degree_current DESC
        LIMIT :lim
        r   u+  
                SELECT e.id::text, e.nif, e.entity_key, e.kind, e.name, e.degree_current,
                       e.natureza_juridica, e.concelho, e.distrito,
                       e.captable_act_id::text, e.captable_as_of,
                       e.captable_newer_act_date, e.captable_total,
                       e.captable_base, e.captable_unidentified,
                       e.captable_currency, e.captable_complete, e.acts_count,
                       e.history_fetched_at,
                       -- quantas quotas do acto é que a fonte não atribui: é a
                       -- diferença entre "falta 90 cêntimos" e "faltam quatro
                       -- quotas que valem 750.000 €"
                       ca.parse_quota_orphans AS captable_orphan_quotas,
                       ca.parse_capital AS captable_act_capital,
                       s.cire_ann_count, s.cire_ann_as_debtor, s.insolvency_open,
                       s.open_claim_deadline, s.process_count, s.risk_score,
                       s.monitored_company_id::text, s.monitoring_type
                  FROM registry_entities e
                  LEFT JOIN registry_acts ca ON ca.id = e.captable_act_id
                  LEFT JOIN entity_incident_summary s ON s.nif = e.nif
                 WHERE e.entity_key = ANY(:keys)
                r   limNnatureza_juridicacaptable_as_ofcaptable_supersededcaptable_superseded_reason
entity_key)executer   mappingsallr"   _captable_supersededget)r   r   r   ordemrowssaidarnos   &&&     r   _nodes_by_keyr4   2   s     & 	 	  
 oo* +0 T?uen2?3
 	
6 hj9 	: (*E!WFZFF&'0@)AG
C !2&B#C "$ao  LG	
s   C9CCA7Czsociedade anonimac          	      z    V ^8  d   QhR\         R,          R\        R\        \        \         R,          3,          /# )r   naturezaNr&   r	   )r
   r   tuplebool)r   s   "r   r   r      s6      Dj*-
4tr   c                N    Ve	   V '       g   R# \        V 4      \        9   d   R# R# )u  A cap table de quotas guardada ainda descreve o presente?

Uma sociedade que se transforma em anónima deixa de ter quotas. As arestas
que temos continuam a ser factos verdadeiros do acto que as publicou — e é o
que permite responder "onde é que este senhor foi sócio" —, mas apresentá-las
como estrutura actual é afirmar uma coisa que deixou de existir.

Foi assim que a JOIN THE MOMENT TRANSITÁRIOS mostrava um sócio de 2011 numa
empresa que é S.A. desde 2022. São 1.020 empresas na mesma situação.
)FN)Tu   sociedade anónima)r   _SEM_QUOTAS)r6   r&   s   &&r   r-   r-      s+     Xh;.))r   c                    V ^8  d   QhR\         R\        \        ,          R\        \        ,          R\        \        \        \        3,          ,          /# )r   r   
entity_ids
edge_typesr	   )r   r    r
   r"   r   )r   s   "r   r   r      sA     # ##'+Cy#>B3i#	$sCx.#r   c                    "   V'       g   . # V P                  \        R 4      RVRV/4      G Rj  xL
 P                  4       P                  4       pV Uu. uF  p\	        V4      NK  	  up#  L;u upi 5i)a  
                SELECT r.id::text, r.edge_type, r.role_label, r.quota_amount,
                       r.currency, r.quota_share, r.valid_from, r.valid_to,
                       r.is_current, r.cease_cause, r.confidence,
                       h.nif AS holder_nif, h.name AS holder_name, h.kind AS holder_kind,
                       h.entity_key AS holder_key,
                       s.nif AS subject_nif, s.name AS subject_name, s.kind AS subject_kind,
                       s.entity_key AS subject_key,
                       a.id::text AS act_id, a.act_type, a.act_date
                  FROM registry_edges r
                  JOIN registry_entities h ON h.id = r.holder_id
                  JOIN registry_entities s ON s.id = r.subject_id
                  JOIN registry_acts a ON a.id = r.source_act_id
                 WHERE r.is_current
                   AND r.edge_type = ANY(:types)
                   AND (r.holder_id = ANY(CAST(:ids AS UUID[]))
                        OR r.subject_id = ANY(CAST(:ids AS UUID[])))
                idstypesNr*   r   r+   r,   r"   )r   r<   r=   r0   r2   s   &&&  r   
_edges_forrB      st      	oo( J4+
 	
. hj1 	2 ""TDGT""1	
0 #s!   -A2A+$A2A-(A2-A2g      N@c                R    V ^8  d   QhR\         R\        \        \        3,          /# r   r   r	   )r   r"   r
   r   )r   s   "r   r   r      s"     " "L "T#s(^ "r   c                T  "   \         P                  ! 4       p\        '       d+   V\        ^ ,          ,
          \        8  d   \        ^,          # V P	                  \        R4      4      G Rj  xL
 P                  4       P                  4       pV'       d   \        V4      M/ pW3sV#  L:5i)uY  O que o índice já cobre.

Vai a par de qualquer resposta que mostre as participações de uma pessoa: essa
pergunta só é respondível pelo índice nacional, e a resposta vale exactamente
o que valer a janela recolhida. Mostrar participações sem dizer o intervalo
coberto convida a ler ausência de dados como ausência de participações.
a  
                SELECT
                  (SELECT min(day) FROM registry_sweep_days WHERE status = 'ok') AS varrido_de,
                  (SELECT max(day) FROM registry_sweep_days WHERE status = 'ok') AS varrido_ate,
                  (SELECT count(*) FROM registry_sweep_days WHERE status = 'ok') AS dias_ok,
                  (SELECT count(*) FROM registry_sweep_days
                    WHERE status IN ('pending','error','partial')) AS dias_em_falta,
                  (SELECT count(*) FROM registry_acts) AS actos,
                  (SELECT count(*) FROM registry_acts WHERE body IS NOT NULL) AS actos_lidos,
                  (SELECT count(*) FROM registry_entities) AS entidades,
                  (SELECT count(*) FROM registry_entities
                    WHERE history_fetched_at IS NOT NULL) AS entidades_com_historico,
                  (SELECT count(*) FROM registry_edges WHERE is_current) AS arestas
                N)	time	monotonic_coverage_cache_COVERAGE_TTLr*   r   r+   firstr"   )r   agorarowouts   &   r   coveragerN      s      NNE5?1#55Eq!!oo
 	
$ hj' ( $s)ClOJ+	
s   A)B(+B&,'B(B(depthr=   include_hubsFc                    V ^8  d   QhR\         R\        R\        R\        \        ,          R,          R\        R\
        \        \        3,          /# )r   r   nifrO   r=   NrP   r	   )r   r
   r!   r    r8   r"   r   )r   s   "r   r   r      s]     y yy	y 	y
 S	D y y 
#s(^yr   c                 "   \        V4      pV'       g   R. R. RRR//# \        ^ \        V\        4      4      pT;'       g    \	        \
        4       Uu. uF  qf\
        9   g   K  VNK  	  ppV P                  \        R4      RV/4      G Rj  xL
 P                  4       pV'       d   \        W.4      G Rj  xL
 M/ p	V'       d   W9  d    R. R. RR	TR
RR\        V 4      G Rj  xL
 //# / p
/ p. pRp\        W,          4      p^ VR&   WV&   V.p\        V4       EFb  pV Uu. uF  pV
V,          R,          NK  	  ppV'       g    EM9\        V4      p\        V VV4      G Rj  xL
 p/ p/ pV Fj  pVVVR,          &   R FU  pVV R2,          pVV9   g   K  VP                  V^ 4      ^,           VV&   VP!                  V. 4      P#                  V4       KW  	  Kl  	  \        4       pVP%                  4        F  w  ppVP                  V^ 4      \&        8  ;'       d    V'       * ;'       d    V^ 8  pV'       d=   VP#                  RV
V,          R,          RV
V,          R,          RVV,          /4       K  V F<  pR F3  pVV R2,          pV'       g   K  VV
9  g   K"  VP)                  V4       K5  	  K>  	  K  	  V'       g    M\        ^ \*        \-        V
4      ,
          4      p\        V \/        V4      VR7      G Rj  xL
 p\-        V4      V8  d   Rp. pVP1                  4        F>  p \        V 4      p V^,           V R&   V V
V R,          &   VP#                  V R,          4       K@  	  V'       g   V'       d   EKc   M	  \        V
4      p!VP1                  4        U"u. uF%  p"V"R,          V!9   g   K  V"R,          V!9   g   K#  V"NK'  	  p#p"R\	        V
P1                  4       4      RT#RR	TR
RRTR\-        V
4      R\-        V#4      RTRTR\&        R\        V 4      G Rj  xL
 /	/# u upi  EL ELu ELKu upi  EL EL:u up"i  L$5i)uB   Grafo em volta de um NIF, com nós, arestas e o que ficou de fora.nodesedgesmetaerroru   NIF inválido9SELECT entity_key FROM registry_entities WHERE nif = :nifrR   NrootfoundFrN   rO   id_keynamedegree)r   Tr)   
holder_keysubject_key
node_count
edge_count	truncatedhubs_skipped
hub_degree)holdersubject)r   maxmin	MAX_DEPTHr    
EDGE_TYPESr*   r   scalarr4   rN   r"   rangesetrB   r.   
setdefaultappenditems
HUB_DEGREEadd	MAX_NODESr   sortedvalues)$r   rR   rO   r=   rP   normtr@   rY   knownrT   rU   hubsrc   nodefrontierlevelnr?   na_fronteirarZ   graupor_noedgesidekey	next_keysligacoeshubotherespacofetcheddatar   ekepts$   &&$$$                               r   neighbourhoodr      s3     S>DWb&7O2LMM3ui()E#77tJ'77L71OQQ7EL
 ooLMDM
 	
 fh 	 59-000bE4$R"VT7E:XgEV?VW
 	

 (*E')E!#DIDDM$KvHu'/0x!uQx~~x08} #u55  "24D $E$t*-dV4=),& $a 01 4DI%%c2.55d;	 .  "e	#\\^MC((3"Z/RR4DRRQRC5:e,feCj6Hd3i   1D D6/Eue!3!e, 2 ! ,  Q	CJ./%gvi/@OOy>F"INN$D:D!AIDM(,E$|$%OOD./	 %
 HHs z u:D<<>!a\?d" 	
'('74'? 	
> 	  	elln%DTU#e*#d)D*hw//

 } M
	

 1 @W 1
 6P P " 0s   A QQP%&P%,"QP*Q)Q8P-9QQ%P0&>Q$P3=(Q%P8&.QBQQ-Q;AQQ $Q<QP;A1Q4Q?!Q P>5P>P>AQQ0Q-Q0Q3Q;Q>Qc                l    V ^8  d   QhR\         R\        R\        \        \        3,          R,          /# )r   r   rR   r	   N)r   r
   r"   r   )r   s   "r   r   r   ^  s3     ~ ~ ~C ~DcNT<Q ~r   c                ,	  "   \        V4      pV'       g   R# V P                  \        R4      RV/4      G Rj  xL
 P                  4       pV'       d   \	        W.4      G Rj  xL
 M/ pV'       d   W49  d   R# \        WC,          4      pV P                  \        R4      RVR,          /4      G Rj  xL
 P                  4       P                  4       pR. R. R. /pRpVP                  R	4      '       dF   V P                  \        R
4      RVRVR	,          RVR,          /4      G Rj  xL
 P                  4       pV'       Edf   V P                  \        R4      RVR,          RV/4      G Rj  xL
 P                  4       P                  4       p	V	 EF  p
\        V
4      p\        VR,          RJ;'       dI    VR,          RJ;'       d6    VR,          ;'       d%    VR,          ;'       d    VR,          VR,          8g  4      VR&   VR,          f&   VR,          e   VR,          P                  V4       K  VR,          f&   VR,          e   VR,          P                  V4       K  VR,          VR,          8w  g   VR,          '       g   K  VR,          P                  V4       EK  	  V P                  \        R4      RVR,          /4      G Rj  xL
 P                  4       P                  4       pV P                  \        R4      RVR,          /4      G Rj  xL
 P                  4       P                  4       pV P                  \        R4      RVR,          /4      G Rj  xL
 P                  4       P                  4       pV P                  \        R4      RV/4      G Rj  xL
 P                  4       P                  4       pRTRV Uu. uF  p\        V4      NK  	  upRTRV Uu. uF  p\        V4      NK  	  upRV Uu. uF  p\        V4      NK  	  upRV Uu. uF  p\        V4      NK  	  upR V Uu. uF  p\        V4      NK  	  upR!\        V 4      G Rj  xL
 /#  EL' EL  EL ELC EL EL EL\ EL Lu upi u upi u upi u upi u upi  L85i)"u   Ficha completa de uma entidade, monitorizada ou não.

É este endpoint que faz o grafo valer a pena: um nó descoberto a dois hops de
distância tem ficha, cap table e incidentes sem ninguém ter de o registar
primeiro.
NrX   rR   a  
                SELECT h.nif, h.name, h.kind,
                       sum(r.quota_amount) AS quota_amount,
                       min(r.currency) AS currency,
                       sum(r.quota_share) AS quota_share,
                       count(*) AS quotas,
                       min(r.valid_from) AS valid_from,
                       max(a.act_type) AS act_type, max(a.act_date) AS act_date,
                       max(a.id::text) AS act_id, min(r.confidence) AS confidence
                  FROM registry_edges r
                  JOIN registry_entities h ON h.id = r.holder_id
                  JOIN registry_acts a ON a.id = r.source_act_id
                 WHERE r.subject_id = CAST(:id AS UUID)
                   AND r.edge_type = 'holds_quota' AND r.is_current
                 GROUP BY h.id, h.nif, h.name, h.kind
                 ORDER BY sum(r.quota_amount) DESC NULLS LAST
                r[   entraramsairam	alteraramcaptable_act_ida  
                    SELECT a.id::text
                      FROM registry_acts a
                     WHERE a.nipc = :nipc
                       AND COALESCE(a.parse_quotas, 0) > 0
                       AND a.id <> CAST(:act AS UUID)
                       AND a.act_date < CAST(:asof AS DATE)
                     ORDER BY COALESCE(a.parse_captable_eligible, FALSE) DESC,
                              a.act_date DESC
                     LIMIT 1
                    nipcactasofr&   a  
                    WITH anterior AS (
                        SELECT r.holder_id,
                               sum(r.quota_amount) AS quota_amount,
                               min(r.currency) AS currency,
                               max(a.act_date) AS act_date
                          FROM registry_edges r
                          JOIN registry_acts a ON a.id = r.source_act_id
                         WHERE r.subject_id = CAST(:id AS UUID)
                           AND r.edge_type = 'holds_quota'
                           AND r.source_act_id = CAST(:prev AS UUID)
                         GROUP BY r.holder_id
                    ),
                    atual AS (
                        SELECT r.holder_id,
                               sum(r.quota_amount) AS quota_amount,
                               min(r.currency) AS currency
                          FROM registry_edges r
                         WHERE r.subject_id = CAST(:id AS UUID)
                           AND r.edge_type = 'holds_quota' AND r.is_current
                         GROUP BY r.holder_id
                    )
                    SELECT h.nif, h.name, h.kind,
                           p.quota_amount AS antes, c.quota_amount AS agora,
                           COALESCE(c.currency, p.currency) AS currency,
                           p.currency AS moeda_anterior,
                           p.act_date AS data_anterior
                      FROM anterior p
                      FULL OUTER JOIN atual c ON c.holder_id = p.holder_id
                      JOIN registry_entities h ON h.id = COALESCE(c.holder_id, p.holder_id)
                     ORDER BY COALESCE(c.quota_amount, p.quota_amount) DESC NULLS LAST
                    prevantesrK   moeda_anteriorcurrencymoeda_mudouac  
                SELECT h.nif, h.name, h.kind, r.edge_type, r.role_label,
                       r.valid_from, r.valid_to, r.is_current, r.cease_cause,
                       a.act_type, a.act_date
                  FROM registry_edges r
                  JOIN registry_entities h ON h.id = r.holder_id
                  JOIN registry_acts a ON a.id = r.source_act_id
                 WHERE r.subject_id = CAST(:id AS UUID)
                   AND r.edge_type NOT IN ('holds_quota', 'spouse_of')
                 ORDER BY r.is_current DESC, r.valid_from DESC NULLS LAST
                 LIMIT 60
                a  
                SELECT s.nif, s.name, s.kind, r.edge_type, r.role_label,
                       sum(r.quota_amount) AS quota_amount,
                       min(r.currency) AS currency,
                       sum(r.quota_share) AS quota_share,
                       count(*) FILTER (WHERE r.edge_type = 'holds_quota') AS quotas,
                       min(r.valid_from) AS valid_from,
                       max(a.act_type) AS act_type, max(a.act_date) AS act_date,
                       bool_or(i.insolvency_open) AS insolvency_open,
                       max(i.cire_ann_as_debtor) AS cire_ann_as_debtor,
                       min(i.monitoring_type) AS monitoring_type
                  FROM registry_edges r
                  JOIN registry_entities s ON s.id = r.subject_id
                  JOIN registry_acts a ON a.id = r.source_act_id
                  LEFT JOIN entity_incident_summary i ON i.nif = s.nif
                 WHERE r.holder_id = CAST(:id AS UUID)
                   AND r.is_current AND r.edge_type <> 'spouse_of'
                 GROUP BY s.id, s.nif, s.name, s.kind, r.edge_type, r.role_label
                 ORDER BY r.edge_type, sum(r.quota_amount) DESC NULLS LAST
                 LIMIT 100
                u  
                -- Uma linha por cônjuge, não uma por acto onde ele aparece. A
                -- aresta repete-se de propósito — cada uma é um facto de um acto
                -- diferente e não se apaga —, mas a mesma pessoa listada seis
                -- vezes lê-se como seis cônjuges.
                SELECT * FROM (
                    SELECT DISTINCT ON (h.id)
                           h.nif, h.name, h.entity_key, r.role_label AS regime,
                           a.act_date,
                           (SELECT count(*) FROM registry_identity_links l
                             WHERE l.unknown_id = h.id AND l.confirmed_at IS NULL
                               AND l.rejected_at IS NULL) AS candidatos
                      FROM registry_edges r
                      JOIN registry_entities h ON h.id = r.holder_id
                      JOIN registry_acts a ON a.id = r.source_act_id
                     WHERE r.subject_id = CAST(:id AS UUID)
                       AND r.edge_type = 'spouse_of'
                     ORDER BY h.id, a.act_date DESC NULLS LAST
                ) t
                 ORDER BY act_date DESC NULLS LAST
                 LIMIT 20
                a  
                SELECT id::text, act_type, act_class, act_date, apresentacao,
                       parse_quotas, parse_people, parse_quota_orphans,
                       parse_capital, parse_captable_complete, parse_error,
                       (body IS NOT NULL) AS lido
                  FROM registry_acts WHERE nipc = :nif
                 ORDER BY act_date DESC NULLS LAST
                 LIMIT 80
                entitycaptablecaptable_changesofficersholdingsspousesactsrN   )r   r*   r   rl   r4   r"   r+   r,   r.   r8   rp   rN   )r   rR   rw   r   baser   r   changesprevious_actpreviousrL   dr   r   r   r   r2   s   &&               r   entity_detailr   ^  sh     S>D ooLMDM
 	
 fh  36w...2D#/$)_F oo& 6$< )
 	
, hj/ < 1;B"k[]/^G Lzz#$$//
 uf->&? 013 " &(% 	& |//!D vd|V\:G$ $J (*SSUM 	N CS	A  $'
$& 9 9gJd*9 9&'9 9 jM9 9 &'1Z=8 Am z!aj&<
#**1-7#'
(>!((+7qz)Q}-=-=$++A.# ( oo 6$< 
 	
" hj% . oo. 6$< 1
 	
4 hj7 < oo0 6$< 3
 	
6 hj9 > oo
 DM
 	
 hj 	$ 	&h/hT!Wh/Gh/hT!Wh/h/hT!Wh/W-WDGW-$'$Qa$'(7++	 	Q	

 /	
T($t	
,	
:	
<	
$ 0//-'+s%  6RQRR"Q"#R1A R1Q%2A R32R%Q(&R)R*Q++ARRR(BR	ARQ.ARQ1ARQ4A RQ7'RQ9
	R#Q>7R>RRR-R4RRR"R"R%R(R+R.R1R4R7R9Rc                    V ^8  d   QhR\         R\        R\        R\        \        \        \
        3,          ,          /# )r   r   queryr   r	   )r   r
   r!   r    r"   r   )r   s   "r   r   r   _  s9     1# 1#1#"%1#.11#	$sCx.1#r   c           	       "   \        V4      pV'       de   V P                  \        R4      RV/4      G Rj  xL
 P                  4       P	                  4       pV'       d   V Uu. uF  p\        V4      NK  	  up# VP                  4       P                  4       pV P                  \        R4      RVRRV R2RV/4      G Rj  xL
 P                  4       P	                  4       pV Uu. uF  p\        V4      NK  	  up#  Lu upi  LBu upi 5i)	z.Pesquisa por NIF exacto ou por nome (trigram).aG  
                    SELECT e.nif, e.name, e.kind, e.concelho, e.degree_current,
                           i.monitoring_type, i.insolvency_open
                      FROM registry_entities e
                      LEFT JOIN entity_incident_summary i ON i.nif = e.nif
                     WHERE e.nif = :nif
                    rR   Na\  
                SELECT e.nif, e.name, e.kind, e.concelho, e.degree_current,
                       i.monitoring_type, i.insolvency_open,
                       GREATEST(similarity(e.name_norm, :q),
                                CASE WHEN e.name_norm ILIKE :like THEN 0.6 ELSE 0 END
                       ) AS score
                  FROM registry_entities e
                  LEFT JOIN entity_incident_summary i ON i.nif = e.nif
                 WHERE e.name_norm ILIKE :like OR e.name_norm % :q
                 ORDER BY score DESC, e.degree_current DESC
                 LIMIT :lim
                qlike%r$   )r   r*   r   r+   r,   r"   lowerstrip)r   r   r   rw   r0   r2   terms   &&&    r   search_entitiesr   _  s      UD//   (*SSU 	 %)*TDGT** ;;= Doo $!D6UE:
 	
" hj% 	& ""TDGT""U +	
$ #sF   4D
C<'D
D
#C>7A	D
 D$D
%D9D
>D
D
c          	      t    V ^8  d   QhR\         R\        R\        \        \        \
        3,          ,          /# )r   r   r   r	   )r   r!   r    r"   r
   r   )r   s   "r   r   r     s/     # # #S #$tCQTH~BV #r   c                   "   V P                  \        R4      RV/4      G Rj  xL
 P                  4       P                  4       pV Uu. uF  p\	        V4      NK  	  up#  L;u upi 5i)u>  Nós por onde começar, para a página da rede não abrir vazia.

Uma página que só mostra uma caixa de pesquisa parece avariada, sobretudo
quando não se sabe o que lá há dentro. Estas são as entidades monitorizadas
com mais ligações conhecidas — que é por onde alguém quereria começar de
qualquer forma.
a  
                SELECT e.nif, e.name, e.kind, e.concelho, e.degree_current,
                       i.monitoring_type, i.insolvency_open,
                       e.captable_as_of
                  FROM registry_entities e
                  JOIN entity_incident_summary i ON i.nif = e.nif
                 WHERE i.monitoring_type IS NOT NULL
                   AND e.degree_current > 0
                 ORDER BY e.degree_current DESC, e.name
                 LIMIT :lim
                r$   NrA   )r   r   r0   r2   s   &&  r   entry_pointsr     sg      oo
 EN
 	
  hj# 	$ ""TDGT""#	
" #s!   !A&A$A&A!A&!A&c                0    V ^8  d   QhR\         R\        /# rD   )r   r!   )r   s   "r   r   r     s     T  T L T S T r   c                ~   "   V P                  \        R4      4      G Rj  xL
 pVP                  ;'       g    ^ #  L5i)uP  Incidentes por NIF, para pintar os nós do grafo.

O `cire_announcement_parties` é **nacional** — 240 296 partes, 55 321 NIFs
distintos — portanto isto funciona para pessoas e entidades que não
monitorizamos, que é precisamente o que faltava. Já `processes` é só das
monitorizadas, e por isso entra como zero em quem não é.
ah  
            WITH cire AS (
                SELECT p.nif,
                       count(*) AS total,
                       count(*) FILTER (
                         WHERE lower(COALESCE(p.role,'')) IN ('insolvente','devedor','requerido')
                       ) AS as_debtor,
                       max(a.pub_date) AS last_date
                  FROM cire_announcement_parties p
                  JOIN cire_announcements a ON a.id = p.announcement_id
                 WHERE p.nif IS NOT NULL
                 GROUP BY p.nif
            ),
            hits AS (
                SELECT h.matched_nif AS nif,
                       min(h.claim_deadline) FILTER (WHERE h.claim_deadline >= CURRENT_DATE)
                         AS open_deadline
                  FROM insolvency_hits h
                 GROUP BY h.matched_nif
            ),
            procs AS (
                SELECT c.nif, count(*) AS total, max(p.date_filed) AS last_date
                  FROM processes p JOIN companies c ON c.id = p.company_id
                 GROUP BY c.nif
            ),
            contratos AS (
                SELECT c.nif, count(*) AS total
                  FROM public_contracts pc JOIN companies c ON c.id = pc.company_id
                 GROUP BY c.nif
            ),
            todos AS (
                SELECT nif FROM cire
                UNION SELECT nif FROM hits
                UNION SELECT nif FROM procs
                UNION SELECT nif FROM contratos
                UNION SELECT nif FROM companies
                UNION SELECT nif FROM registry_entities WHERE nif IS NOT NULL
            )
            INSERT INTO entity_incident_summary (
                nif, cire_ann_count, cire_ann_as_debtor, cire_ann_last_date,
                insolvency_open, open_claim_deadline, process_count, process_last_date,
                contracts_count, risk_score, monitored_company_id, monitoring_type,
                computed_at
            )
            SELECT t.nif,
                   COALESCE(ci.total, 0), COALESCE(ci.as_debtor, 0), ci.last_date,
                   (h.open_deadline IS NOT NULL), h.open_deadline,
                   COALESCE(pr.total, 0), pr.last_date,
                   COALESCE(co.total, 0),
                   co2.risk_score, co2.id, co2.monitoring_type,
                   now()
              FROM todos t
              LEFT JOIN cire ci ON ci.nif = t.nif
              LEFT JOIN hits h ON h.nif = t.nif
              LEFT JOIN procs pr ON pr.nif = t.nif
              LEFT JOIN contratos co ON co.nif = t.nif
              LEFT JOIN companies co2 ON co2.nif = t.nif
             WHERE t.nif IS NOT NULL AND length(trim(t.nif)) = 9
            ON CONFLICT (nif) DO UPDATE SET
                cire_ann_count = EXCLUDED.cire_ann_count,
                cire_ann_as_debtor = EXCLUDED.cire_ann_as_debtor,
                cire_ann_last_date = EXCLUDED.cire_ann_last_date,
                insolvency_open = EXCLUDED.insolvency_open,
                open_claim_deadline = EXCLUDED.open_claim_deadline,
                process_count = EXCLUDED.process_count,
                process_last_date = EXCLUDED.process_last_date,
                contracts_count = EXCLUDED.contracts_count,
                risk_score = EXCLUDED.risk_score,
                monitored_company_id = EXCLUDED.monitored_company_id,
                monitoring_type = EXCLUDED.monitoring_type,
                computed_at = now()
            N)r*   r   rowcount)r   results   & r   refresh_incident_summaryr     sG      ??GI	
K KFX ??aYKs   =;==c                    V ^8  d   Qh/ ^ \         9   d3   \        \        \        \        \
        3,          3,          R,          ;R&   # )r   NrH   )__conditional_annotations__r7   floatr"   r
   r   )r   s   "r   r   r      s7      r < ;ud38n,-4 ;sr   )holds_quotamanagesaudits	secretary
liquidator
depositary	spouse_ofr   r   )   )   )!r   __doc__loggingrF   typingr   
sqlalchemyr   sqlalchemy.ext.asyncior    app.services.registry_act_parserr   	getLogger__name__loggerrj   rt   rr   rk   r   r4   r:   r-   rB   rI   rH   rN   r   r   r   r   r   r   )r   s   @r   <module>r      s   (     / ;			8	$		

0DT ##(#L 7; ;"Jy 	y
 $(y yx~B1#h#:T r   