fix(address+ui): remove TIER 2 reuse, typed diff badges, false positive reduction

- Remove TIER 2 address lookup (county+city without street) from PL/SQL — creates
  new address when street differs instead of reusing wrong one
- Replace generic "N diferente" badge with typed micro-badges (CUI, Denumire, TVA,
  Adr. livr., Adr. fact., Preturi) with red/amber semantic colors
- Extend addrMatch() regex to strip full Romanian address words (STRADA, NUMAR, BLOC,
  COMUNA, SAT, MUNICIPIUL, etc.) — fixes "Strada X" vs "X" false positives
- Extend normalize_company_name() for II, PFA, INTREPRINDERE INDIVIDUALA legal forms
- Persist address_mismatch to SQLite so "Dif." filter includes address-only diffs
- Add red/amber indicator dots to desktop table and mobile list rows
- 12 unit tests for normalization and server-side address matching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-06 14:28:57 +00:00
parent fc1013bff6
commit 31095c07f7
11 changed files with 160 additions and 44 deletions

View File

@@ -352,6 +352,7 @@ def init_sqlite():
("adresa_facturare_roa", "TEXT"), ("adresa_facturare_roa", "TEXT"),
("anaf_denumire_mismatch", "INTEGER DEFAULT 0"), ("anaf_denumire_mismatch", "INTEGER DEFAULT 0"),
("denumire_anaf", "TEXT"), ("denumire_anaf", "TEXT"),
("address_mismatch", "INTEGER DEFAULT 0"),
]: ]:
if col not in order_cols: if col not in order_cols:
conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}") conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}")

View File

@@ -183,8 +183,10 @@ def normalize_company_name(name: str) -> str:
result = name.strip().upper() result = name.strip().upper()
# Strip diacritics # Strip diacritics
result = result.translate(_DIACRITICS) result = result.translate(_DIACRITICS)
# Remove common suffixes # Remove common suffixes and legal forms
result = re.sub(r'\b(S\.?R\.?L\.?|S\.?A\.?|S\.?C\.?|S\.?N\.?C\.?|S\.?C\.?S\.?)\b', '', result) result = re.sub(r'\b(S\.?R\.?L\.?|S\.?A\.?|S\.?C\.?|S\.?N\.?C\.?|S\.?C\.?S\.?|P\.?F\.?A\.?|INTREPRINDERE\s+INDIVIDUALA)\b', '', result)
# Strip II only at start of name (avoid matching Roman numeral II in "TEHNICA II SRL")
result = re.sub(r'^I\.?I\.?\s+', '', result)
# Remove punctuation and extra spaces # Remove punctuation and extra spaces
result = re.sub(r'[^\w\s]', '', result) result = re.sub(r'[^\w\s]', '', result)
result = re.sub(r'\s+', ' ', result).strip() result = re.sub(r'\s+', ' ', result).strip()

View File

@@ -697,7 +697,7 @@ async def get_orders(page: int = 1, per_page: int = 50,
if status_filter.upper() == "IMPORTED": if status_filter.upper() == "IMPORTED":
data_clauses.append("UPPER(status) IN ('IMPORTED', 'ALREADY_IMPORTED')") data_clauses.append("UPPER(status) IN ('IMPORTED', 'ALREADY_IMPORTED')")
elif status_filter.upper() == "DIFFS": elif status_filter.upper() == "DIFFS":
data_clauses.append("(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1)") data_clauses.append("(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1 OR address_mismatch = 1)")
else: else:
data_clauses.append("UPPER(status) = ?") data_clauses.append("UPPER(status) = ?")
data_params.append(status_filter.upper()) data_params.append(status_filter.upper())
@@ -751,9 +751,9 @@ async def get_orders(page: int = 1, per_page: int = 50,
cursor = await db.execute(f"SELECT COUNT(*) FROM orders {uninv_old_where}", base_params) cursor = await db.execute(f"SELECT COUNT(*) FROM orders {uninv_old_where}", base_params)
uninvoiced_old = (await cursor.fetchone())[0] uninvoiced_old = (await cursor.fetchone())[0]
# Diffs count: orders with ANAF adjustments # Diffs count: orders with ANAF adjustments or address mismatches
diffs_clauses = list(base_clauses) + [ diffs_clauses = list(base_clauses) + [
"(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1)" "(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1 OR address_mismatch = 1)"
] ]
diffs_where = "WHERE " + " AND ".join(diffs_clauses) diffs_where = "WHERE " + " AND ".join(diffs_clauses)
cursor = await db.execute(f"SELECT COUNT(*) FROM orders {diffs_where}", base_params) cursor = await db.execute(f"SELECT COUNT(*) FROM orders {diffs_where}", base_params)
@@ -1138,6 +1138,7 @@ async def update_order_partner_data(order_number: str, partner_data: dict):
adresa_facturare_roa = ?, adresa_facturare_roa = ?,
anaf_denumire_mismatch = ?, anaf_denumire_mismatch = ?,
denumire_anaf = ?, denumire_anaf = ?,
address_mismatch = ?,
updated_at = datetime('now') updated_at = datetime('now')
WHERE order_number = ? WHERE order_number = ?
""", ( """, (
@@ -1153,6 +1154,7 @@ async def update_order_partner_data(order_number: str, partner_data: dict):
partner_data.get("adresa_facturare_roa"), partner_data.get("adresa_facturare_roa"),
partner_data.get("anaf_denumire_mismatch", 0), partner_data.get("anaf_denumire_mismatch", 0),
partner_data.get("denumire_anaf"), partner_data.get("denumire_anaf"),
partner_data.get("address_mismatch", 0),
order_number, order_number,
)) ))
await db.commit() await db.commit()

View File

@@ -1,6 +1,8 @@
import asyncio import asyncio
import json import json
import logging import logging
import re
import unicodedata
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@@ -18,6 +20,34 @@ from .. import database
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _addr_match(gomag_json, roa_json):
"""Server-side address comparison matching JS addrMatch()."""
if not gomag_json or not roa_json:
return True
try:
g = json.loads(gomag_json) if isinstance(gomag_json, str) else gomag_json
r = json.loads(roa_json) if isinstance(roa_json, str) else roa_json
except (json.JSONDecodeError, TypeError):
return True
_ADDR_WORDS = re.compile(
r'\b(STR|STRADA|NR|NUMAR|NUMARUL|BL|BLOC|SC|SCARA|AP|APART|APARTAMENT|'
r'ET|ETAJ|COM|COMUNA|SAT|MUN|MUNICIPIUL|JUD|JUDETUL|CARTIER|PARTER|SECTOR|ORAS)\b'
)
def norm(s):
s = unicodedata.normalize('NFD', s or '')
s = re.sub(r'[\u0300-\u036f]', '', s).upper()
s = _ADDR_WORDS.sub('', s)
return re.sub(r'[^A-Z0-9]', '', s)
g_street = norm(g.get('address') or g.get('strada') or '')
r_street = norm((r.get('strada') or '') + (r.get('numar') or ''))
g_city = norm(g.get('city') or g.get('localitate') or '')
r_city = norm(r.get('localitate') or '')
g_region = norm(g.get('region') or g.get('judet') or '')
r_region = norm(r.get('judet') or '')
return g_street == r_street and g_city == r_city and g_region == r_region
# Sync state # Sync state
_sync_lock = asyncio.Lock() _sync_lock = asyncio.Lock()
_current_sync = None # dict with run_id, status, progress info _current_sync = None # dict with run_id, status, progress info
@@ -802,6 +832,11 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
partner_data["anaf_denumire_mismatch"] = 1 partner_data["anaf_denumire_mismatch"] = 1
partner_data["denumire_anaf"] = anaf_data_for_order["denumire_anaf"] partner_data["denumire_anaf"] = anaf_data_for_order["denumire_anaf"]
# Address mismatch check (server-side, mirrors JS addrMatch)
livr_match = _addr_match(partner_data.get("adresa_livrare_gomag"), partner_data.get("adresa_livrare_roa"))
fact_match = _addr_match(partner_data.get("adresa_facturare_gomag"), partner_data.get("adresa_facturare_roa"))
partner_data["address_mismatch"] = 1 if (not livr_match or not fact_match) else 0
await sqlite_service.update_order_partner_data(order.number, partner_data) await sqlite_service.update_order_partner_data(order.number, partner_data)
if not result["success"]: if not result["success"]:

View File

@@ -1176,6 +1176,11 @@ tr.mapping-deleted td {
.anaf-badge-warn { background: var(--warning-light); color: var(--warning-text); } .anaf-badge-warn { background: var(--warning-light); color: var(--warning-text); }
.anaf-badge-gray { background: var(--cancelled-light); color: var(--text-muted); } .anaf-badge-gray { background: var(--cancelled-light); color: var(--text-muted); }
/* Diff-type badges (reuses .anaf-badge sizing per DESIGN.md type scale minimum) */
.diff-badge { display:inline-block; font-family:var(--font-body); font-size:12px; font-weight:500; padding:2px 8px; border-radius:9999px; margin-left:4px; vertical-align:middle; }
.diff-badge-anaf { background:var(--error-light); color:var(--error-text); }
.diff-badge-info { background:var(--warning-light); color:var(--warning-text); }
/* ── Compact order detail layout ──────────────── */ /* ── Compact order detail layout ──────────────── */
.detail-col-label { .detail-col-label {
font-family: var(--font-display); font-family: var(--font-display);

View File

@@ -368,7 +368,7 @@ async function loadDashOrders() {
<td>${statusDot(o.status)}</td> <td>${statusDot(o.status)}</td>
<td class="text-nowrap">${dateStr}</td> <td class="text-nowrap">${dateStr}</td>
${renderClientCell(o)} ${renderClientCell(o)}
<td><code>${esc(o.order_number)}</code></td> <td><code>${esc(o.order_number)}</code>${(o.anaf_cod_fiscal_adjusted===1||o.anaf_denumire_mismatch===1||(o.cod_fiscal_gomag&&o.anaf_platitor_tva===0))?'<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--error);margin-left:4px;vertical-align:middle" title="Diferente ANAF"></span>':''}${(o.address_mismatch===1||o.price_match===false)?'<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--warning);margin-left:2px;vertical-align:middle" title="Diferente adresa/pret"></span>':''}</td>
<td>${o.items_count || 0}</td> <td>${o.items_count || 0}</td>
<td class="text-end text-muted">${fmtCost(o.delivery_cost)}</td> <td class="text-end text-muted">${fmtCost(o.delivery_cost)}</td>
<td class="text-end text-muted">${fmtCost(o.discount_total)}</td> <td class="text-end text-muted">${fmtCost(o.discount_total)}</td>
@@ -394,12 +394,14 @@ async function loadDashOrders() {
} }
const name = o.customer_name || o.shipping_name || o.billing_name || '\u2014'; const name = o.customer_name || o.shipping_name || o.billing_name || '\u2014';
const totalStr = o.order_total ? Number(o.order_total).toFixed(2) : ''; const totalStr = o.order_total ? Number(o.order_total).toFixed(2) : '';
const anafDiffDot = (o.anaf_cod_fiscal_adjusted===1||o.anaf_denumire_mismatch===1||(o.cod_fiscal_gomag&&o.anaf_platitor_tva===0)) ? '<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--error);margin-right:2px;vertical-align:middle" title="ANAF"></span>' : '';
const addrDiffDot = o.address_mismatch===1 ? '<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--warning);margin-right:2px;vertical-align:middle" title="Adresa"></span>' : '';
const priceMismatch = o.price_match === false ? '<span class="dot dot-red" style="width:6px;height:6px" title="Pret!="></span> ' : ''; const priceMismatch = o.price_match === false ? '<span class="dot dot-red" style="width:6px;height:6px" title="Pret!="></span> ' : '';
return `<div class="flat-row" onclick="openDashOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem"> return `<div class="flat-row" onclick="openDashOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem">
${statusDot(o.status)} ${statusDot(o.status)}
<span style="color:var(--text-muted)" class="text-nowrap">${dateFmt}</span> <span style="color:var(--text-muted)" class="text-nowrap">${dateFmt}</span>
<span class="grow truncate fw-bold">${esc(name)}</span> <span class="grow truncate fw-bold">${esc(name)}</span>
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · ' + priceMismatch + '<strong>' + totalStr + '</strong>' : ''}</span> <span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · ' + anafDiffDot + addrDiffDot + priceMismatch + '<strong>' + totalStr + '</strong>' : ''}</span>
</div>`; </div>`;
}).join(''); }).join('');
} }

View File

@@ -856,7 +856,7 @@ function addrMatch(gomag, roa) {
function norm(s) { function norm(s) {
return (s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '') return (s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.toUpperCase() .toUpperCase()
.replace(/\b(STR|NR|BL|SC|AP|ET|ETAJ|APART)\b/g, '') .replace(/\b(STR|STRADA|NR|NUMAR|NUMARUL|BL|BLOC|SC|SCARA|AP|APART|APARTAMENT|ET|ETAJ|COM|COMUNA|SAT|MUN|MUNICIPIUL|JUD|JUDETUL|CARTIER|PARTER|SECTOR|ORAS)\b/g, '')
.replace(/[^A-Z0-9]/g, ''); .replace(/[^A-Z0-9]/g, '');
} }
const gStreet = norm(gomag.address || gomag.strada || ''); const gStreet = norm(gomag.address || gomag.strada || '');
@@ -1013,22 +1013,25 @@ function _renderHeaderInfo(order) {
addressLines.innerHTML = html; addressLines.innerHTML = html;
// Diff summary badge in modal header // Typed diff badges in modal header
let diffCount = 0;
if (isPJ && pi.anaf_denumire_mismatch) diffCount++;
if (order.price_check && order.price_check.mismatches > 0) diffCount += order.price_check.mismatches;
if (addr) {
if (addr.livrare_roa && !addrMatch(addr.livrare_gomag, addr.livrare_roa)) diffCount++;
if (addr.facturare_roa && !addrMatch(addr.facturare_gomag, addr.facturare_roa)) diffCount++;
}
const orderNumEl = document.getElementById('detailOrderNumber'); const orderNumEl = document.getElementById('detailOrderNumber');
if (orderNumEl && diffCount > 0) { if (orderNumEl) {
const existing = orderNumEl.querySelector('.diff-badge'); orderNumEl.parentNode.querySelectorAll('.diff-badge').forEach(b => b.remove());
if (existing) existing.remove(); const badges = [];
const badge = document.createElement('span'); if (isPJ && pi.anaf_cod_fiscal_adjusted) badges.push({label:'CUI', cls:'diff-badge-anaf', aria:'CUI ajustat conform ANAF'});
badge.className = 'diff-badge badge ms-2'; if (isPJ && pi.anaf_denumire_mismatch) badges.push({label:'Denumire', cls:'diff-badge-anaf', aria:'Denumire diferita fata de ANAF'});
badge.style.cssText = 'background:var(--warning-light);color:var(--warning-text);font-size:11px;vertical-align:middle'; if (isPJ && pi.anaf_platitor_tva === 0) badges.push({label:'TVA', cls:'diff-badge-anaf', aria:'Neplatitor TVA conform ANAF'});
badge.textContent = diffCount + ' diferente'; if (addr && addr.livrare_roa && !addrMatch(addr.livrare_gomag, addr.livrare_roa)) badges.push({label:'Adr. livr.', cls:'diff-badge-info', aria:'Adresa livrare diferita'});
orderNumEl.parentNode.insertBefore(badge, orderNumEl.nextSibling); if (addr && addr.facturare_roa && !addrMatch(addr.facturare_gomag, addr.facturare_roa)) badges.push({label:'Adr. fact.', cls:'diff-badge-info', aria:'Adresa facturare diferita'});
if (order.price_check && order.price_check.mismatches > 0) badges.push({label:'Preturi (' + order.price_check.mismatches + ')', cls:'diff-badge-info', aria:'Preturi diferite: ' + order.price_check.mismatches});
let insertAfter = orderNumEl;
badges.forEach(b => {
const el = document.createElement('span');
el.className = 'diff-badge ' + b.cls;
el.setAttribute('aria-label', b.aria);
el.textContent = b.label;
insertAfter.parentNode.insertBefore(el, insertAfter.nextSibling);
insertAfter = el;
});
} }
} }

View File

@@ -19,7 +19,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
{% set rp = request.scope.get('root_path', '') %} {% set rp = request.scope.get('root_path', '') %}
<link href="{{ rp }}/static/css/style.css?v=34" rel="stylesheet"> <link href="{{ rp }}/static/css/style.css?v=35" rel="stylesheet">
</head> </head>
<body> <body>
<!-- Top Navbar (hidden on mobile via CSS) --> <!-- Top Navbar (hidden on mobile via CSS) -->
@@ -161,7 +161,7 @@
<script>window.ROOT_PATH = "{{ rp }}";</script> <script>window.ROOT_PATH = "{{ rp }}";</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ rp }}/static/js/shared.js?v=25"></script> <script src="{{ rp }}/static/js/shared.js?v=26"></script>
<script> <script>
// Dark mode toggle // Dark mode toggle
function toggleDarkMode() { function toggleDarkMode() {

View File

@@ -115,5 +115,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=36"></script> <script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=37"></script>
{% endblock %} {% endblock %}

View File

@@ -6,6 +6,7 @@ CREATE OR REPLACE PACKAGE PACK_IMPORT_PARTENERI AS
-- 02.04.2026 - cautare CUI strict (p_strict_search=1) sau dual anti-dedup (NULL) -- 02.04.2026 - cautare CUI strict (p_strict_search=1) sau dual anti-dedup (NULL)
-- 02.04.2026 - parser adrese: extrage APARTAMENT/SCARA/ETAJ embedded in strada (fix "Nr17 apartament 8") -- 02.04.2026 - parser adrese: extrage APARTAMENT/SCARA/ETAJ embedded in strada (fix "Nr17 apartament 8")
-- 02.04.2026 - fallback cautare PF cu permutari nume (evita duplicate la swap firstname/lastname) -- 02.04.2026 - fallback cautare PF cu permutari nume (evita duplicate la swap firstname/lastname)
-- 06.04.2026 - eliminat TIER 2 cautare adresa (judet+loc fara strada) — creeaza adresa noua cand strada difera
-- ==================================================================== -- ====================================================================
-- CONSTANTS -- CONSTANTS
@@ -948,23 +949,6 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_PARTENERI AS
when NO_DATA_FOUND then p_id_adresa := null; when NO_DATA_FOUND then p_id_adresa := null;
end; 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;
end;
end if;
-- Adaug o adresa -- Adaug o adresa
if p_id_adresa is null then if p_id_adresa is null then
-- caut judetul -- caut judetul

View File

@@ -634,3 +634,85 @@ class TestGetPricesForOrderCantitateRoa:
assert result["items"][0]["match"] is None assert result["items"][0]["match"] is None
assert result["items"][0]["kit"] is True assert result["items"][0]["kit"] is True
assert result["summary"]["mismatches"] == 0 assert result["summary"]["mismatches"] == 0
# ── normalize_company_name (II, PFA, INTREPRINDERE INDIVIDUALA) ──
class TestNormalizeCompanyNameExtended:
"""Tests for extended legal form stripping in normalize_company_name."""
def test_strip_ii_at_start(self):
from app.services.anaf_service import normalize_company_name
assert normalize_company_name("II CHIRITA N ION") == "CHIRITA N ION"
def test_no_strip_ii_mid_name(self):
from app.services.anaf_service import normalize_company_name
result = normalize_company_name("TEHNICA II SRL")
assert "II" in result # II should remain (Roman numeral)
def test_strip_pfa(self):
from app.services.anaf_service import normalize_company_name
assert normalize_company_name("PFA POPESCU ION") == "POPESCU ION"
def test_strip_intreprindere_individuala(self):
from app.services.anaf_service import normalize_company_name
result = normalize_company_name("CHIRIȚĂ N. ION ÎNTREPRINDERE INDIVIDUALĂ")
assert "CHIRITA N ION" == result
def test_strip_ii_with_dots(self):
from app.services.anaf_service import normalize_company_name
assert normalize_company_name("I.I. CHIRITA N ION") == "CHIRITA N ION"
# ── _addr_match (server-side address comparison) ─<><E29480><EFBFBD>
class TestAddrMatch:
"""Tests for _addr_match server-side address comparison."""
def test_matching_addresses(self):
from app.services.sync_service import _addr_match
import json
g = json.dumps({"address": "Str. Elisabeta 10", "city": "Bucuresti", "region": "Bucuresti"})
r = json.dumps({"strada": "Elisabeta", "numar": "10", "localitate": "Bucuresti", "judet": "Bucuresti"})
assert _addr_match(g, r) is True
def test_street_mismatch(self):
from app.services.sync_service import _addr_match
import json
g = json.dumps({"address": "Str. Elisabeta 10", "city": "Bucuresti", "region": "Bucuresti"})
r = json.dumps({"strada": "Victoriei", "numar": "10", "localitate": "Bucuresti", "judet": "Bucuresti"})
assert _addr_match(g, r) is False
def test_city_mismatch(self):
from app.services.sync_service import _addr_match
import json
g = json.dumps({"address": "Elisabeta 10", "city": "Brasov", "region": "Brasov"})
r = json.dumps({"strada": "Elisabeta", "numar": "10", "localitate": "Cluj", "judet": "Brasov"})
assert _addr_match(g, r) is False
def test_county_mismatch(self):
from app.services.sync_service import _addr_match
import json
g = json.dumps({"address": "Elisabeta 10", "city": "Brasov", "region": "Brasov"})
r = json.dumps({"strada": "Elisabeta", "numar": "10", "localitate": "Brasov", "judet": "Cluj"})
assert _addr_match(g, r) is False
def test_none_input_returns_true(self):
from app.services.sync_service import _addr_match
assert _addr_match(None, None) is True
assert _addr_match(None, '{"strada":"x"}') is True
assert _addr_match('{"address":"x"}', None) is True
def test_strada_prefix_stripping(self):
"""'Strada Elisabeta' should match 'ELISABETA' after normalization."""
from app.services.sync_service import _addr_match
import json
g = json.dumps({"address": "Strada Elisabeta 10", "city": "Bucuresti", "region": "Bucuresti"})
r = json.dumps({"strada": "Elisabeta", "numar": "10", "localitate": "Bucuresti", "judet": "Bucuresti"})
assert _addr_match(g, r) is True
def test_malformed_json_returns_true(self):
from app.services.sync_service import _addr_match
assert _addr_match("{bad json", '{"strada":"x"}') is True