fix(address): smart parser splits numar into bloc/scara/apart/etaj (ORA-12899)

Addresses with commas after street (e.g. "Str.Dacia NR.15 BLOC Z2,SCARA A,AP.7")
caused NUMAR column overflow (max 10 chars). Parser now tokenizes by comma and
routes BL/SC/AP/ET/NR prefixes to proper columns. Also extracts NR/BLOC embedded
in street text. Import service now blocks orders when address creation fails
(returns ERROR instead of silently importing without address).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-01 09:19:47 +00:00
parent 115666155b
commit e478c35ecd
3 changed files with 170 additions and 49 deletions

View File

@@ -307,6 +307,16 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
])
addr_livr_id = id_adresa_livr.getvalue()
if addr_livr_id is None:
cur.execute("SELECT PACK_IMPORT_PARTENERI.get_last_error FROM dual")
plsql_err = cur.fetchone()[0]
err_msg = f"Shipping address creation failed for partner {partner_id}"
if plsql_err:
err_msg += f": {plsql_err}"
logger.error(f"Order {order_number}: {err_msg}")
result["error"] = err_msg
return result
# Step 3: Process billing address
if different_person:
# Different person: use shipping address for BOTH billing and shipping in ROA
@@ -325,6 +335,16 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
])
addr_fact_id = id_adresa_fact.getvalue()
if addr_fact_id is None:
cur.execute("SELECT PACK_IMPORT_PARTENERI.get_last_error FROM dual")
plsql_err = cur.fetchone()[0]
err_msg = f"Billing address creation failed for partner {partner_id}"
if plsql_err:
err_msg += f": {plsql_err}"
logger.error(f"Order {order_number}: {err_msg}")
result["error"] = err_msg
return result
if addr_fact_id is not None:
result["id_adresa_facturare"] = int(addr_fact_id)
if addr_livr_id is not None:

View File

@@ -1,6 +1,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)
-- ====================================================================
-- CONSTANTS
@@ -89,7 +90,11 @@ CREATE OR REPLACE PACKAGE PACK_IMPORT_PARTENERI AS
p_localitate OUT VARCHAR2,
p_strada OUT VARCHAR2,
p_numar OUT VARCHAR2,
p_sector OUT VARCHAR2);
p_sector OUT VARCHAR2,
p_bloc OUT VARCHAR2,
p_scara OUT VARCHAR2,
p_apart OUT VARCHAR2,
p_etaj OUT VARCHAR2);
-- ====================================================================
-- UTILITY FUNCTIONS (PUBLIC pentru testare)
@@ -380,91 +385,91 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_PARTENERI AS
p_prenume := NULL;
END separa_nume_prenume;
-- 31.03.2026 - parser inteligent: split numar in bloc/scara/apart/etaj (fix ORA-12899 pe NUMAR max 10 chars)
PROCEDURE parseaza_adresa_semicolon(p_adresa_text IN VARCHAR2,
p_judet OUT VARCHAR2,
p_localitate OUT VARCHAR2,
p_strada OUT VARCHAR2,
p_numar OUT VARCHAR2,
p_sector OUT VARCHAR2) IS
p_sector OUT VARCHAR2,
p_bloc OUT VARCHAR2,
p_scara OUT VARCHAR2,
p_apart OUT VARCHAR2,
p_etaj OUT VARCHAR2) IS
v_adresa_curata VARCHAR2(500);
v_componente SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST();
v_count NUMBER;
v_temp_judet VARCHAR2(100);
v_pozitie NUMBER;
v_strada VARCHAR2(100);
-- variabile pentru parsarea inteligenta a numarului
v_raw_numar VARCHAR2(500);
v_token VARCHAR2(200);
v_token_upper VARCHAR2(200);
v_rest_parts VARCHAR2(500);
v_tok_pos NUMBER;
v_tok_idx NUMBER;
BEGIN
-- p_adresa_text: JUD: JUDET;LOCALITATE;STRADA, NR
-- Initializare cu valori default
p_judet := C_JUD_DEFAULT;
p_localitate := C_LOCALITATE_DEFAULT;
p_strada := NULL;
p_numar := NULL;
p_sector := C_SECTOR_DEFAULT;
p_bloc := NULL;
p_scara := NULL;
p_apart := NULL;
p_etaj := NULL;
-- Validare input
IF p_adresa_text IS NULL THEN
-- pINFO('Adresa goala, se folosesc valorile default', 'IMPORT_PARTENERI');
RETURN;
END IF;
v_adresa_curata := TRIM(p_adresa_text);
-- pINFO('Parsare adresa: ' || v_adresa_curata, 'IMPORT_PARTENERI');
-- Split dupa semicolon
SELECT TRIM(REGEXP_SUBSTR(v_adresa_curata, '[^;]+', 1, LEVEL))
BULK COLLECT
INTO v_componente
FROM DUAL
CONNECT BY REGEXP_SUBSTR(v_adresa_curata, '[^;]+', 1, LEVEL) IS NOT NULL;
v_count := v_componente.COUNT;
IF v_count = 0 THEN
-- pINFO('Nu s-au gasit componente in adresa', 'IMPORT_PARTENERI');
RETURN;
END IF;
-- Parsare in functie de numarul de componente
IF v_count = 1 THEN
-- Doar strada
p_strada := SUBSTR(v_componente(1), 1, 100);
ELSIF v_count = 2 THEN
-- Localitate;Strada
p_localitate := SUBSTR(v_componente(1), 1, 100);
p_strada := SUBSTR(v_componente(2), 1, 100);
ELSIF v_count >= 3 THEN
-- Verifica daca prima componenta contine "JUD:"
v_temp_judet := v_componente(1);
IF UPPER(v_temp_judet) LIKE 'JUD:%' THEN
-- Format: JUD:Bucuresti;BUCURESTI;Strada,Numar
p_judet := SUBSTR(REPLACE(v_temp_judet, 'JUD:', ''), 1, 100);
p_localitate := SUBSTR(v_componente(2), 1, 100);
p_strada := SUBSTR(v_componente(3), 1, 100);
v_strada := p_strada;
-- Combina strada si numarul
-- Separa strada de tot ce e dupa prima virgula
v_pozitie := INSTR(v_strada, ',');
IF v_pozitie > 0 THEN
p_strada := TRIM(SUBSTR(v_strada, 1, v_pozitie - 1));
p_numar := TRIM(SUBSTR(v_strada, v_pozitie + 1));
-- Elimina prefixele din numele strazii (STR., STRADA, BD., BDUL., etc.)
/* v_nume_strada := TRIM(REGEXP_REPLACE(v_nume_strada,
'^(STR\.|STRADA|BD\.|BDUL\.|CALEA|PIATA|PTA\.|AL\.|ALEEA|SOS\.|SOSEA|INTR\.|INTRAREA)\s*',
'', 1, 1, 'i')); */
-- Elimina prefixele din numarul strazii (NR., NUMARUL, etc.)
p_numar := TRIM(REGEXP_REPLACE(p_numar,
'^(NR\.|NUMARUL|NUMAR)\s*',
'',
1,
1,
'i'));
p_strada := TRIM(SUBSTR(v_strada, 1, v_pozitie - 1));
v_raw_numar := TRIM(SUBSTR(v_strada, v_pozitie + 1));
END IF;
ELSE
-- Format: Localitate;Strada;Altceva
p_localitate := SUBSTR(v_componente(1), 1, 100);
@@ -473,35 +478,116 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_PARTENERI AS
100);
END IF;
END IF;
-- Pre-processing: extrage NR/BLOC embedded in p_strada (spatiu-separate, fara virgula)
-- Ex: "STR.DACIA NR.15 BLOC Z2" → strada="STR.DACIA", numar="15", bloc="Z2"
-- Trebuie facut INAINTE de parsarea tokenilor din v_raw_numar
IF p_strada IS NOT NULL THEN
v_token_upper := UPPER(p_strada);
-- Extrage NR din strada
IF REGEXP_LIKE(v_token_upper, '(\s)(NUMARUL|NUMAR|NR\.?)\s*(\S+)') THEN
p_numar := TRIM(REGEXP_REPLACE(v_token_upper, '.*(\s)(NUMARUL|NUMAR|NR\.?)\s*(\S+).*', '\3', 1, 1));
p_strada := TRIM(REGEXP_REPLACE(p_strada, '(\s)(NUMARUL|NUMAR|NR\.?)\s*\S+', '', 1, 1, 'i'));
END IF;
-- Extrage BLOC din strada
IF REGEXP_LIKE(v_token_upper, '(\s)(BLOC|BL\.?)\s*(\S+)') THEN
p_bloc := TRIM(REGEXP_REPLACE(v_token_upper, '.*(\s)(BLOC|BL\.?)\s*(\S+).*', '\3', 1, 1));
p_strada := TRIM(REGEXP_REPLACE(p_strada, '(\s)(BLOC|BL\.?)\s*\S+', '', 1, 1, 'i'));
END IF;
END IF;
-- ================================================================
-- Parser inteligent: split v_raw_numar in numar/bloc/scara/apart/etaj
-- Tokenii sunt separati prin virgula
-- Patterns: NR/NUMAR, BL/BLOC, SC/SCARA, AP/APART, ET/ETAJ
-- ================================================================
IF v_raw_numar IS NOT NULL THEN
-- Loop prin tokeni separati de virgula (fara BULK COLLECT — compatibil Oracle 11)
v_rest_parts := NULL;
v_tok_idx := 0;
v_raw_numar := v_raw_numar || ','; -- sentinel pentru ultimul token
LOOP
v_tok_pos := INSTR(v_raw_numar, ',');
EXIT WHEN v_tok_pos = 0 OR v_raw_numar IS NULL;
v_token := TRIM(SUBSTR(v_raw_numar, 1, v_tok_pos - 1));
v_raw_numar := SUBSTR(v_raw_numar, v_tok_pos + 1);
v_tok_idx := v_tok_idx + 1;
IF v_token IS NULL THEN
CONTINUE;
END IF;
v_token_upper := UPPER(v_token);
-- Longer match first; (\s|\.) handles both "BL A2" and "BL.A2" and "AP.7"
IF REGEXP_LIKE(v_token_upper, '^(BLOC|BL\.?)(\s|\.)') THEN
p_bloc := TRIM(REGEXP_REPLACE(v_token, '^(BLOC|BL\.?)(\s|\.)*', '', 1, 1, 'i'));
ELSIF REGEXP_LIKE(v_token_upper, '^(SCARA|SC\.?)(\s|\.)') THEN
p_scara := TRIM(REGEXP_REPLACE(v_token, '^(SCARA|SC\.?)(\s|\.)*', '', 1, 1, 'i'));
ELSIF REGEXP_LIKE(v_token_upper, '^(APARTAMENT|APART\.?|AP\.?)(\s|\.)') THEN
p_apart := TRIM(REGEXP_REPLACE(v_token, '^(APARTAMENT|APART\.?|AP\.?)(\s|\.)*', '', 1, 1, 'i'));
ELSIF REGEXP_LIKE(v_token_upper, '^(ETAJ|ET\.?)(\s|\.)') THEN
p_etaj := TRIM(REGEXP_REPLACE(v_token, '^(ETAJ|ET\.?)(\s|\.)*', '', 1, 1, 'i'));
ELSIF REGEXP_LIKE(v_token_upper, '^(NUMARUL|NUMAR|NR\.?)(\s|\.)') THEN
p_numar := TRIM(REGEXP_REPLACE(v_token, '^(NUMARUL|NUMAR|NR\.?)(\s|\.)*', '', 1, 1, 'i'));
ELSE
-- Primul token necunoscut devine numar (daca numar e inca gol)
IF p_numar IS NULL AND v_tok_idx = 1 THEN
p_numar := v_token;
ELSE
-- Restul (cartier, sat, indicatii) se adauga la strada
IF v_rest_parts IS NOT NULL THEN
v_rest_parts := v_rest_parts || ', ' || v_token;
ELSE
v_rest_parts := v_token;
END IF;
END IF;
END IF;
END LOOP;
-- Adauga restul la strada
IF v_rest_parts IS NOT NULL THEN
p_strada := SUBSTR(p_strada || ', ' || v_rest_parts, 1, 100);
END IF;
END IF;
-- Curatare finala
p_judet := UPPER(TRIM(p_judet));
p_localitate := UPPER(TRIM(p_localitate));
p_strada := UPPER(TRIM(p_strada));
p_numar := UPPER(TRIM(p_numar));
p_sector := UPPER(TRIM(p_sector));
p_bloc := UPPER(TRIM(p_bloc));
p_scara := UPPER(TRIM(p_scara));
p_apart := UPPER(TRIM(p_apart));
p_etaj := UPPER(TRIM(p_etaj));
-- Truncare de siguranta (limita coloanelor Oracle)
p_numar := SUBSTR(p_numar, 1, 10);
p_bloc := SUBSTR(p_bloc, 1, 30);
p_scara := SUBSTR(p_scara, 1, 10);
p_apart := SUBSTR(p_apart, 1, 10);
p_etaj := SUBSTR(p_etaj, 1, 20);
-- Fallback pentru campuri goale
IF p_judet IS NULL THEN
p_judet := C_JUD_DEFAULT;
END IF;
IF p_localitate IS NULL THEN
p_localitate := C_LOCALITATE_DEFAULT;
END IF;
IF p_sector IS NULL THEN
p_sector := C_SECTOR_DEFAULT;
END IF;
-- pINFO('Adresa parsata: JUD=' || p_judet || ', LOC=' || p_localitate ||
-- ', STRADA=' || NVL(p_strada, 'NULL') || ', SECTOR=' || p_sector, 'IMPORT_PARTENERI');
EXCEPTION
WHEN OTHERS THEN
g_last_error := 'ERROR in parseaza_adresa_semicolon: ' || SQLERRM;
-- pINFO('ERROR in parseaza_adresa_semicolon: ' || SQLERRM, 'IMPORT_PARTENERI');
-- Pastram valorile default in caz de eroare
p_judet := C_JUD_DEFAULT;
p_localitate := C_LOCALITATE_DEFAULT;
@@ -676,6 +762,10 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_PARTENERI AS
v_strada VARCHAR2(1000);
v_numar VARCHAR2(1000);
v_sector VARCHAR2(100);
v_bloc VARCHAR2(30);
v_scara VARCHAR2(10);
v_apart VARCHAR2(10);
v_etaj VARCHAR2(20);
v_id_tara NUMBER(10);
v_principala NUMBER(1);
begin
@@ -695,13 +785,17 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_PARTENERI AS
where id_part = p_id_part
and principala = 1);
-- Parseaza adresa
-- Parseaza adresa (cu split inteligent numar/bloc/scara/apart/etaj)
parseaza_adresa_semicolon(p_adresa,
v_judet,
v_localitate,
v_strada,
v_numar,
v_sector);
v_sector,
v_bloc,
v_scara,
v_apart,
v_etaj);
-- caut prima adresa dupa judet si localitate, ordonate dupa principala = 1
begin
@@ -782,10 +876,10 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_PARTENERI AS
tnDA_apare => 0,
tcStrada => v_strada,
tcNumar => v_numar,
tcBloc => NULL,
tcScara => NULL,
tcApart => NULL,
tnEtaj => NULL,
tcBloc => v_bloc,
tcScara => v_scara,
tcApart => v_apart,
tnEtaj => v_etaj,
tnId_loc => v_id_localitate,
tcLocalitate => v_localitate,
tnId_judet => v_id_judet,