feat(anaf-dedup): ANAF partner dedup + address fix + UI enrichment

Prevent partner duplicates via ANAF CUI verification and dual PL/SQL
search. Fix address matching with street-level comparison and diacritics
normalization. Show partner/address comparison in order detail modal.

- New anaf_service.py: batch ANAF API client with chunking, retry, cache
- PL/SQL: dual CUI search (bare/RO+bare/RO space+bare), 3-tier address
  search (street+city+id_loc → city+id_loc → create), strip_diacritics
  at storage for addresses and partner names
- SQLite: anaf_cache table, 12 new order columns for partner/address data
- import_service: cod_fiscal_override param, return partner/address from Oracle
- sync_service: ANAF batch integration, denomination mismatch detection,
  cache pre-population trigger
- Router: enriched order_detail with partner_info + addresses JSON
- UI: collapsible Detalii Partener + Adrese Comparativ sections in modal,
  auto-expand on mismatch, ANAF badges, mobile address cards
- Dashboard: address quality attention indicator
- New scan_duplicate_partners.py script for one-time duplicate audit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-01 14:36:52 +00:00
parent 3b9198d742
commit 2f593c30f6
12 changed files with 925 additions and 64 deletions

View File

@@ -2,6 +2,7 @@ CREATE OR REPLACE PACKAGE PACK_IMPORT_PARTENERI AS
-- 20.03.2026 - import parteneri GoMag: PJ/PF, shipping/billing, cautare/creare automata
-- 31.03.2026 - parser inteligent adrese: split numar in bloc/scara/apart/etaj (fix ORA-12899 pe NUMAR max 10 chars)
-- 01.04.2026 - ANAF dedup: cautare duala CUI, adrese pe strada+diacritics, strip diacritics la stocare
-- ====================================================================
-- CONSTANTS
@@ -146,10 +147,25 @@ CREATE OR REPLACE PACKAGE PACK_IMPORT_PARTENERI AS
*/
PROCEDURE clear_error;
FUNCTION strip_diacritics(p_text IN VARCHAR2) RETURN VARCHAR2;
END PACK_IMPORT_PARTENERI;
/
CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_PARTENERI AS
-- 01.04.2026 - strip_diacritics la stocare adrese si parteneri
FUNCTION strip_diacritics(p_text IN VARCHAR2) RETURN VARCHAR2 IS
BEGIN
IF p_text IS NULL THEN
RETURN NULL;
END IF;
RETURN TRANSLATE(
UPPER(TRIM(p_text)),
'ĂăÂâÎîȘșȚțŞşŢţ',
'AAAAIISSTTSSTT'
);
END strip_diacritics;
-- ================================================================
-- ERROR MANAGEMENT FUNCTIONS IMPLEMENTATION
-- ================================================================
@@ -212,57 +228,52 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_PARTENERI AS
-- PUBLIC FUNCTIONS IMPLEMENTATION
-- ====================================================================
-- 01.04.2026 - cautare duala cod_fiscal cu/fara prefix RO (anti-duplicare parteneri)
FUNCTION cauta_partener_dupa_cod_fiscal(p_cod_fiscal IN VARCHAR2)
RETURN NUMBER IS
v_id_part NUMBER;
v_cod_fiscal_curat VARCHAR2(50);
v_bare_cui VARCHAR2(50);
v_ro_cui VARCHAR2(52);
BEGIN
-- Validare input
IF p_cod_fiscal IS NULL OR
LENGTH(TRIM(p_cod_fiscal)) < C_MIN_COD_FISCAL THEN
IF p_cod_fiscal IS NULL OR LENGTH(TRIM(p_cod_fiscal)) < C_MIN_COD_FISCAL THEN
RETURN NULL;
END IF;
v_cod_fiscal_curat := curata_text_cautare(p_cod_fiscal);
-- pINFO('Cautare partener dupa cod_fiscal: ' || v_cod_fiscal_curat, 'IMPORT_PARTENERI');
-- Cautare in NOM_PARTENERI
v_cod_fiscal_curat := UPPER(TRIM(p_cod_fiscal));
-- Extract bare CUI (without RO prefix)
IF REGEXP_LIKE(v_cod_fiscal_curat, '^RO\s*\d') THEN
v_bare_cui := TRIM(REGEXP_REPLACE(v_cod_fiscal_curat, '^RO\s*', ''));
ELSE
v_bare_cui := v_cod_fiscal_curat;
END IF;
v_ro_cui := 'RO' || v_bare_cui;
-- 01.04.2026 - cautare duala cod_fiscal cu/fara prefix RO (anti-duplicare parteneri)
-- Search 3 forms: bare, RO+bare, RO+space+bare (index-friendly)
-- Priority: active + exact form > active + alternate > inactive
BEGIN
SELECT id_part
INTO v_id_part
SELECT id_part INTO v_id_part FROM (
SELECT id_part
FROM nom_parteneri
WHERE UPPER(TRIM(cod_fiscal)) = v_cod_fiscal_curat
AND ROWNUM = 1; -- In caz de duplicate, luam primul
-- pINFO('Gasit partener cu cod_fiscal ' || v_cod_fiscal_curat || ': ID_PART=' || v_id_part, 'IMPORT_PARTENERI');
WHERE UPPER(TRIM(cod_fiscal)) IN (v_bare_cui, v_ro_cui, 'RO ' || v_bare_cui)
AND NVL(sters, 0) = 0
ORDER BY NVL(inactiv, 0) ASC,
CASE WHEN UPPER(TRIM(cod_fiscal)) = v_cod_fiscal_curat THEN 0 ELSE 1 END ASC,
id_part DESC
) WHERE ROWNUM = 1;
RETURN v_id_part;
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- pINFO('Nu s-a gasit partener cu cod_fiscal: ' || v_cod_fiscal_curat, 'IMPORT_PARTENERI');
RETURN NULL;
WHEN TOO_MANY_ROWS THEN
-- Luam primul gasit
SELECT id_part
INTO v_id_part
FROM (SELECT id_part
FROM nom_parteneri
WHERE UPPER(TRIM(cod_fiscal)) = v_cod_fiscal_curat
ORDER BY id_part)
WHERE ROWNUM = 1;
pINFO('WARNING: Multiple parteneri cu acelasi cod_fiscal ' ||
v_cod_fiscal_curat || '. Selectat ID_PART=' || v_id_part,
'IMPORT_PARTENERI');
RETURN v_id_part;
END;
EXCEPTION
WHEN OTHERS THEN
pINFO('ERROR in cauta_partener_dupa_cod_fiscal: ' || SQLERRM,
'IMPORT_PARTENERI');
pINFO('ERROR in cauta_partener_dupa_cod_fiscal: ' || SQLERRM, 'IMPORT_PARTENERI');
RAISE;
END cauta_partener_dupa_cod_fiscal;
@@ -677,6 +688,9 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_PARTENERI AS
-- pINFO('Nume separat: NUME=' || NVL(v_nume, 'NULL') || ', PRENUME=' || NVL(v_prenume, 'NULL'), 'IMPORT_PARTENERI');
END IF;
-- Strip diacritics from partner name before storage
v_denumire_curata := strip_diacritics(v_denumire_curata);
-- Creare partener prin pack_def
BEGIN
IF v_este_persoana_fizica = 1 THEN
@@ -797,30 +811,37 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_PARTENERI AS
v_apart,
v_etaj);
-- caut prima adresa dupa judet si localitate, ordonate dupa principala = 1
-- 01.04.2026 - cautare adresa pe strada + diacritics + id_loc validation
-- TIER 1: county + city + street (diacritics normalized) + valid id_loc
begin
select max(id_adresa) over(order by principala desc)
into p_id_adresa
from vadrese_parteneri
where id_part = p_id_part
and judet = v_judet
and localitate = v_localitate;
exception
WHEN NO_DATA_FOUND THEN
p_id_adresa := null;
end;
-- caut prima adresa dupa judet, ordonate dupa principala = 1
if p_id_adresa is null then
begin
select max(id_adresa) over(order by principala desc)
into p_id_adresa
select id_adresa into p_id_adresa from (
select id_adresa
from vadrese_parteneri
where id_part = p_id_part
and judet = v_judet;
and judet = v_judet
and localitate = v_localitate
and strip_diacritics(strada) = strip_diacritics(v_strada)
and id_loc IS NOT NULL
order by principala desc, id_adresa desc
) where rownum = 1;
exception
when NO_DATA_FOUND then p_id_adresa := null;
end;
-- TIER 2: county + city (no street) but ONLY with valid id_loc
if p_id_adresa is null then
begin
select id_adresa into p_id_adresa from (
select id_adresa
from vadrese_parteneri
where id_part = p_id_part
and judet = v_judet
and localitate = v_localitate
and id_loc IS NOT NULL
order by principala desc, id_adresa desc
) where rownum = 1;
exception
WHEN NO_DATA_FOUND THEN
p_id_adresa := null;
when NO_DATA_FOUND then p_id_adresa := null;
end;
end if;
@@ -870,6 +891,12 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_PARTENERI AS
end;
end;
-- 01.04.2026 - strip_diacritics la stocare adrese
v_strada := strip_diacritics(v_strada);
v_localitate := strip_diacritics(v_localitate);
v_numar := strip_diacritics(v_numar);
v_bloc := strip_diacritics(v_bloc);
BEGIN
pack_def.adauga_adresa_partener2(tnId_part => p_id_part,
tcDenumire_adresa => NULL,