diff --git a/TODOS.md b/TODOS.md index 0b864f2..4138a8a 100644 --- a/TODOS.md +++ b/TODOS.md @@ -13,3 +13,10 @@ **Effort:** M (human: ~1 sapt / CC: ~1h) **Context:** Depinde de infrastructura email/webhook disponibila la client. Implementare: SMTP simplu sau webhook URL configurabil in Settings. **Depends on:** Lansare in productie + infrastructura email la client. + +## P3: Fix script — handle missing orders in GoMag API +**What:** Fix script for 17 address-less orders should check if GoMag API returns data for each order, and report which orders couldn't be fixed. +**Why:** Old orders may be deleted or expired from GoMag API. Without this check, the fix script fails silently and the operator thinks all 17 were fixed. +**Effort:** S (human: ~10min / CC: ~2min) +**Context:** Part of the address overflow fix (Pas 5). The fix script re-downloads from GoMag API to get original address text, but doesn't verify the API response. Add empty-response check + report. +**Depends on:** Address parser fix (Pas 1-2) deployed. diff --git a/api/app/services/import_service.py b/api/app/services/import_service.py index 610a604..8219a51 100644 --- a/api/app/services/import_service.py +++ b/api/app/services/import_service.py @@ -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: diff --git a/api/database-scripts/05_pack_import_parteneri.pck b/api/database-scripts/05_pack_import_parteneri.pck index 6aa9898..3f13318 100644 --- a/api/database-scripts/05_pack_import_parteneri.pck +++ b/api/database-scripts/05_pack_import_parteneri.pck @@ -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,