Skip to content

Commit de85f74

Browse files
committed
fix(uyap): shared-Esas card location by KAYIT NO + genuine identity by KAYIT NO
Bulk run on all 13 Ankara sold cards produced 5 RECONCILIATION_FAILED, all on shared-Esas files (2026/12 x3, 2026/94, 2026/1). Root cause: find_target_record_card/_locate_card_control located the card by Esas (file_id) only; for a file with multiple auctions they grabbed the WRONG card's documents, so the collected docs' asset didn't match the target -> the reconciliation guard correctly rejected them (no silent bad admission). Fix A (card location): threaded target_record_ref (KAYIT NO) through _collect_documents -> find_target_record_card + _locate_card_control (and bulk _acquire/process_sold_auction/diagnose_documents pass kayit_no). find_target_record_card now selects, among same-Esas cards, the one whose KAYIT NO matches (visible text OR class 'incelenen-li {NO}'); if none match it returns None (never grabs the wrong card). _locate_card_control scopes the live card by KAYIT NO (text filter, then class-attribute ancestor fallback). No record_ref -> original Esas-only behavior (pilot backward-compatible). Fix B (genuine identity): build_genuine_record public_record_id now uses the KAYIT NO (candidate.kayit_no) not the Esas file_id. This matches the existing 7 genuine records (11-digit KAYIT NO ids) AND prevents distinct shared-Esas auctions from collapsing to one record on admission (idempotent dedup is per KAYIT NO). Province still filled from bulk label; card 'Satış Tutarı' never used. Tests: shared-Esas card selection (by KAYIT NO; no-match -> None; no-ref -> backward compat), genuine public_record_id = KAYIT NO (distinct per auction). 529 uyap/extract/reconcile/model/pilot/admit tests pass.
1 parent f4b74cd commit de85f74

5 files changed

Lines changed: 158 additions & 42 deletions

File tree

src/sold/ingestion/uyap/admit.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ def build_genuine_record(candidate: dict) -> dict:
6767
f"domain=uyap, sale_mechanism=auction, reference_price_type=appraisal -> excluded from asking_to_closing_labels()."
6868
)
6969
return {
70-
"public_record_id": candidate.get("file_id"),
70+
"public_record_id": (candidate.get("kayit_no") or (candidate.get("bulk") or {}).get("kayit_no")
71+
or candidate.get("file_id")),
7172
"auction_date": _iso_date(ev.get("completion_datetime")),
7273
"province": ev.get("province") or _bulk_province(candidate),
7374
"district": ev.get("district"),

src/sold/ingestion/uyap/bulk.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ def process_sold_auction(
782782

783783
# 3) ÇALIŞAN belge-edinim yolunu yeniden kullan (yeni bir parser DEĞİL).
784784
try:
785-
artifacts, patterns, diag = acquire_documents(file_id, institution)
785+
artifacts, patterns, diag = acquire_documents(file_id, institution, record_ref=kayit_no)
786786
except Exception as exc: # edinim hatası kaydı; sonraki açık artırmanın kimliğini BOZMAZ
787787
existing.setdefault("bulk", {})["last_acquisition_error"] = str(exc)[:160]
788788
store.log_event(existing, "bulk_acquisition_failed", str(exc)[:160])
@@ -958,7 +958,7 @@ def diagnose_documents(self, province, date_from, date_to,
958958
target = next((c for c in cards if c.get("sold")), None)
959959
fid = target.get("file_id") if target else None
960960
pre_tabs = [self._safe_ref(p.url) for p in context.pages]
961-
docs, patterns, diag = collector._collect_documents(page, context, fid, province, native_only=True)
961+
docs, patterns, diag = collector._collect_documents(page, context, fid, province, native_only=True, target_record_ref=target_kayit_no)
962962
post_tabs = [self._safe_ref(p.url) for p in context.pages]
963963
keys = ("page_state", "document_entry_path", "target_record_card_found",
964964
"document_list_control_found", "document_list_control_kind", "document_list_opened",
@@ -1051,8 +1051,9 @@ def run(
10511051
"Önce UYAP e-Satış → İhaleler → Geçmiş İlanlar sayfasını elle açın."
10521052
)
10531053

1054-
def _acquire(file_id, institution):
1055-
return collector._collect_documents(page, context, file_id, institution, native_only=True)
1054+
def _acquire(file_id, institution, record_ref=None):
1055+
return collector._collect_documents(page, context, file_id, institution,
1056+
native_only=True, target_record_ref=record_ref)
10561057

10571058
state = load_bulk_state(self.store_dir)
10581059
for w in windows:

src/sold/ingestion/uyap/collect.py

Lines changed: 103 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1419,11 +1419,31 @@ def _card_control_labels(card_html: str) -> list[str]:
14191419
return []
14201420

14211421

1422-
def find_target_record_card(html: str, target_file_id: str, institution: str | None = None) -> dict | None:
1422+
def _card_matches_record_ref(card_el, record_ref: str | None) -> bool:
1423+
"""Kart, KAYIT NO'yu (record_ref) içeriyor mu — görünür metinde YA DA class/id niteliğinde
1424+
(UYAP 'incelenen-li {NO}'). PAYLAŞILAN-Esas'ta DOĞRU açık artırma kartını seçmek için."""
1425+
ref = re.sub(r"\D", "", str(record_ref or ""))
1426+
if not ref:
1427+
return False
1428+
if ref in re.sub(r"\D", "", card_el.get_text(" ", strip=True)):
1429+
return True
1430+
for el in [card_el] + card_el.find_all(True):
1431+
cls = el.get("class") or []
1432+
if isinstance(cls, str):
1433+
cls = [cls]
1434+
for token in list(cls) + [el.get("id") or ""]:
1435+
if ref in re.sub(r"\D", "", str(token)):
1436+
return True
1437+
return False
1438+
1439+
1440+
def find_target_record_card(html: str, target_file_id: str, institution: str | None = None,
1441+
target_record_ref: str | None = None) -> dict | None:
14231442
"""Listeleme sayfasında HEDEF kaydı resmî dosya kimliğiyle bulur (fiyat/P/Q/nth DEĞİL).
14241443
1425-
Aynı dosya kimliğine sahip TEK kartı döndürür; birden çok distinkt dosya no içeren container
1426-
(tüm-liste) satır/kart SAYILMAZ. İlk-global-kart ya da nth-kart seçilmez. OFFLINE testable.
1444+
``target_record_ref`` (KAYIT NO) verilmişse aynı Esas'ı paylaşan kartlar arasından o KAYIT NO'lu
1445+
kartı seçer (PAYLAŞILAN-Esas'ta yanlış-kart edinimini önler; eşleşen kart yoksa dürüstçe None);
1446+
verilmemişse aynı dosya kimliğine sahip TEK kartı döndürür. Çok-kayıtlı container SAYILMAZ. OFFLINE testable.
14271447
"""
14281448
try:
14291449
from bs4 import BeautifulSoup
@@ -1446,16 +1466,25 @@ def find_target_record_card(html: str, target_file_id: str, institution: str | N
14461466
inst_tok = _ascii_lower(institution).split()
14471467
if inst_tok and inst_tok[0] in _ascii_lower(text):
14481468
match_fields.append("institution")
1449-
matches.append((c, re.sub(r"\s+", " ", text), match_fields))
1450-
if len(matches) >= 1:
1451-
c, text, match_fields = matches[0]
1452-
return {
1453-
"html": str(c),
1454-
"file_text": text[:120],
1455-
"match_fields": match_fields,
1456-
"control_labels": _card_control_labels(str(c)),
1457-
"target_file": tnum,
1458-
}
1469+
ref_match = _card_matches_record_ref(c, target_record_ref)
1470+
matches.append((c, re.sub(r"\s+", " ", text), match_fields, ref_match))
1471+
if not matches:
1472+
continue
1473+
if target_record_ref:
1474+
ref_hits = [m for m in matches if m[3]]
1475+
if not ref_hits:
1476+
continue # bu selector'da KAYIT NO eşleşen kart YOK → yanlış kartı ALMA, sonrakini dene
1477+
c, text, match_fields, _ = ref_hits[0]
1478+
match_fields = match_fields + ["record_ref"]
1479+
else:
1480+
c, text, match_fields, _ = matches[0]
1481+
return {
1482+
"html": str(c),
1483+
"file_text": text[:120],
1484+
"match_fields": match_fields,
1485+
"control_labels": _card_control_labels(str(c)),
1486+
"target_file": tnum,
1487+
}
14591488
return None
14601489

14611490

@@ -1798,7 +1827,7 @@ def _select_target_page(self, context, target_file_id=None): # pragma: no cover
17981827
page = context.pages[idx] if 0 <= idx < len(context.pages) else context.pages[0]
17991828
return page, sel
18001829

1801-
def _collect_documents(self, page, context, target_file_id=None, target_institution=None, native_only=False) -> tuple: # pragma: no cover - canlı DOM/olay gerektirir
1830+
def _collect_documents(self, page, context, target_file_id=None, target_institution=None, native_only=False, target_record_ref=None) -> tuple: # pragma: no cover - canlı DOM/olay gerektirir
18021831
"""SAYFA-DURUMU FARKINDA, HEDEF-KAYIT-KAPSAMLI belge girişi (iki gerçek gözlenen yol).
18031832
18041833
``search_listing`` → HEDEF kayıt kartı dosya kimliğiyle bulunur (fiyat/nth DEĞİL) →
@@ -1851,7 +1880,7 @@ def _collect_documents(self, page, context, target_file_id=None, target_institut
18511880
diag["document_collection_attempts"].append({"stage": "target_card", "blocking_reason": "no_target_file_id_provided"})
18521881
diag["document_collection_failures"] += 1
18531882
return documents, patterns, diag
1854-
card = find_target_record_card(html, target_file_id, target_institution)
1883+
card = find_target_record_card(html, target_file_id, target_institution, target_record_ref)
18551884
if card is None:
18561885
diag["document_collection_attempts"].append({"stage": "target_card", "blocking_reason": "target_record_card_not_found_on_listing"})
18571886
diag["document_collection_failures"] += 1
@@ -1869,7 +1898,7 @@ def _collect_documents(self, page, context, target_file_id=None, target_institut
18691898
return documents, patterns, diag
18701899
# Kart-yerel kontrolü CANLI DOM'da HEDEF kart kapsamında bul (global text locator DEĞİL —
18711900
# üçüncü canlı hatanın kök nedeni buydu).
1872-
control = self._locate_card_control(page, target_file_id)
1901+
control = self._locate_card_control(page, target_file_id, target_record_ref)
18731902
elif page_state == "record_detail":
18741903
if target_file_id and not file_identity_matches(html, target_file_id):
18751904
diag["document_collection_attempts"].append({"stage": "detail_identity", "blocking_reason": "detail_page_does_not_match_target_file_id"})
@@ -1982,32 +2011,72 @@ def _collect_documents(self, page, context, target_file_id=None, target_institut
19822011
patterns += p2
19832012
return documents, patterns, diag
19842013

1985-
def _locate_card_control(self, page, target_file_id): # pragma: no cover - canlı DOM
1986-
"""HEDEF kaydın CANLI kartını dosya kimliğiyle bulur; kart-yerel "İhale Evrak Listesi"
1987-
kontrolünü döner (global metin locator DEĞİL — kart kapsamı zorunlu)."""
2014+
def _locate_card_control(self, page, target_file_id, target_record_ref=None): # pragma: no cover - canlı DOM
2015+
"""HEDEF kaydın CANLI kartını bulur; kart-yerel "İhale Evrak Listesi" kontrolünü döner.
2016+
2017+
``target_record_ref`` (KAYIT NO) verilmişse PAYLAŞILAN-Esas'ta kart o KAYIT NO ile daraltılır
2018+
(yalnız Esas ile filtreleme çok-açık-artırmalı dosyada YANLIŞ kartı seçerdi → RECONCILIATION_FAILED).
2019+
KAYIT NO görünür metinde YA DA class ('incelenen-li {NO}') olabilir; eşleşen kart yoksa None (yanlış
2020+
kartı ALMA). Global metin locator DEĞİL — kart kapsamı zorunlu."""
19882021
import re as _re
19892022

19902023
tnum = normalize_file_identity(target_file_id)
19912024
key = _re.escape(tnum).replace("/", r"\s*/\s*")
1992-
for csel in ("[class*=card]", "[class*=sonuc]", "[class*=result]", "[class*=ilan]", "li", "tr"):
1993-
try:
1994-
cards = page.locator(csel).filter(has_text=_re.compile(key, _re.I))
1995-
if cards.count() == 0:
1996-
continue
1997-
card = cards.first
1998-
for name in ("button", "link"):
1999-
try:
2000-
act = card.get_by_role(name, name=_re.compile("evrak listesi", _re.I))
2001-
if act.count() > 0:
2002-
return act.first
2003-
except Exception:
2004-
continue
2025+
ref = _re.sub(r"\D", "", str(target_record_ref or ""))
2026+
card_selectors = ("[class*=card]", "[class*=sonuc]", "[class*=result]", "[class*=ilan]", "li", "tr")
2027+
2028+
def _control_in(card):
2029+
for name in ("button", "link"):
20052030
try:
2006-
act = card.get_by_text(_re.compile("evrak listesi", _re.I))
2031+
act = card.get_by_role(name, name=_re.compile("evrak listesi", _re.I))
20072032
if act.count() > 0:
20082033
return act.first
20092034
except Exception:
2010-
pass
2035+
continue
2036+
try:
2037+
act = card.get_by_text(_re.compile("evrak listesi", _re.I))
2038+
if act.count() > 0:
2039+
return act.first
2040+
except Exception:
2041+
pass
2042+
return None
2043+
2044+
if ref:
2045+
# (a) KAYIT NO görünür metinde: Esas + KAYIT NO ile daralt
2046+
for csel in card_selectors:
2047+
try:
2048+
cc = page.locator(csel).filter(has_text=_re.compile(key, _re.I)).filter(has_text=_re.compile(ref))
2049+
if cc.count() > 0:
2050+
ctrl = _control_in(cc.first)
2051+
if ctrl is not None:
2052+
return ctrl
2053+
except Exception:
2054+
continue
2055+
# (b) KAYIT NO class niteliğinde ('incelenen-li {NO}'): o elemanın evrak-kontrolü içeren kart atası
2056+
try:
2057+
holder = page.locator(f'[class*="{ref}"]')
2058+
if holder.count() > 0:
2059+
for xp in ('xpath=ancestor-or-self::*[.//*[contains(@id,"detailButton")]][1]',
2060+
'xpath=ancestor-or-self::*[.//a or .//button][1]'):
2061+
try:
2062+
ctrl = _control_in(holder.first.locator(xp))
2063+
if ctrl is not None:
2064+
return ctrl
2065+
except Exception:
2066+
continue
2067+
except Exception:
2068+
pass
2069+
return None # KAYIT NO verildi ama DOĞRU kart bulunamadı → yanlış kartı ALMA (dürüst)
2070+
2071+
# KAYIT NO yok → orijinal Esas-yalnız davranış (pilot geriye-uyumlu)
2072+
for csel in card_selectors:
2073+
try:
2074+
cards = page.locator(csel).filter(has_text=_re.compile(key, _re.I))
2075+
if cards.count() == 0:
2076+
continue
2077+
ctrl = _control_in(cards.first)
2078+
if ctrl is not None:
2079+
return ctrl
20112080
except Exception:
20122081
continue
20132082
return None

tests/test_uyap_bulk.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def _sold_card(fid="2026/263", kayit="16701234"):
271271
}
272272

273273

274-
def _fake_acquire_ok(file_id, institution):
274+
def _fake_acquire_ok(file_id, institution, record_ref=None):
275275
text = (
276276
"Artırma Sonuç Tutanağı İhale Bedeli 5.715.000,00 TL "
277277
"Muhammen Bedel 6.800.000,00 TL Satıldı Satış İşlemleri Tamamlandı "
@@ -281,7 +281,7 @@ def _fake_acquire_ok(file_id, institution):
281281
return arts, [{"label": "auction_result", "pattern": "native_udf"}], {"ok": True}
282282

283283

284-
def _fake_acquire_fail(file_id, institution):
284+
def _fake_acquire_fail(file_id, institution, record_ref=None):
285285
raise RuntimeError("same_row_download_control_not_located_uniquely")
286286

287287

@@ -378,7 +378,7 @@ def test_force_reacquires_known_candidate_but_not_admitted(tmp_path):
378378
# force ile → edinici GERÇEKTEN yeniden çağrılır ve yeniden edinilir
379379
calls = {"n": 0}
380380

381-
def _counting_acquire(file_id, institution):
381+
def _counting_acquire(file_id, institution, record_ref=None):
382382
calls["n"] += 1
383383
return _fake_acquire_ok(file_id, institution)
384384

@@ -398,6 +398,24 @@ def _counting_acquire(file_id, institution):
398398
assert guarded["outcome"] == "skipped_already_acquired"
399399

400400

401+
def test_genuine_public_record_id_uses_kayit_no_not_esas():
402+
# PAYLAŞILAN-Esas: genuine kimlik KAYIT NO olmalı (mevcut 7 kayıt 11-haneli KAYIT NO kullanır);
403+
# public_record_id=Esas olsaydı aynı Esas'ın farklı açık artırmaları admitte TEK kayda ÇÖKERdi.
404+
from sold.ingestion.uyap.admit import build_genuine_record
405+
from sold.ingestion.uyap.models import ADMISSIBLE_COMPLETED_SALE
406+
cand = {
407+
"file_id": "2026/12", "kayit_no": "16760703981",
408+
"extracted": {"property_type": "konut", "completion_datetime": "16.06.2026"},
409+
"audit": {"decision": ADMISSIBLE_COMPLETED_SALE, "appraisal_value": 2000000.0, "auction_price": 1800000.0},
410+
"bulk": {"province_label": "ANKARA", "kayit_no": "16760703981"},
411+
}
412+
rec = build_genuine_record(cand)
413+
assert rec["public_record_id"] == "16760703981" # KAYIT NO, Esas değil
414+
assert rec["province"] == "Ankara"
415+
cand2 = dict(cand, kayit_no="16760703976", bulk={"province_label": "ANKARA", "kayit_no": "16760703976"})
416+
assert build_genuine_record(cand2)["public_record_id"] == "16760703976" # aynı Esas, farklı kayıt → çökmez
417+
418+
401419
# --------------------------------------------------------------------------- #
402420
# 9) Modal hatası — bir açık artırmanın hatası sonrakinin kimliğini/durumunu bozmaz.
403421
# --------------------------------------------------------------------------- #

tests/test_uyap_live_fix3.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,33 @@ def test_multiple_fake_cards_choose_correct_target():
149149
assert "2025/100" not in card["file_text"] and "2024/99" not in card["file_text"]
150150

151151

152+
def _listing_page_shared_esas():
153+
"""PAYLAŞILAN-Esas: aynı Esas (2026/12) iki farklı açık artırma (KAYIT NO class 'incelenen-li {NO}')."""
154+
return (
155+
'<html><body><div class="results-list">'
156+
'<div class="ilan-card"><span class="incelenen-li 16760703976"></span>'
157+
'Ankara İcra Dairesi 2026/12 İcra Muhammen Bedel 1.000.000,00 TL '
158+
'<button>İncele</button><button>İhale Evrak Listesi</button></div>'
159+
'<div class="ilan-card"><span class="incelenen-li 16760703981"></span>'
160+
'Ankara İcra Dairesi 2026/12 İcra Muhammen Bedel 2.000.000,00 TL '
161+
'<button>İncele</button><button>İhale Evrak Listesi</button></div>'
162+
'</div></body></html>'
163+
)
164+
165+
166+
def test_shared_esas_selects_card_by_kayit_no():
167+
html = _listing_page_shared_esas()
168+
# KAYIT NO olmadan → ilk kart (geriye-uyumlu; record_ref eşleşmesi yok)
169+
c0 = find_target_record_card(html, "2026/12")
170+
assert c0 is not None and "record_ref" not in c0["match_fields"]
171+
# KAYIT NO ile → DOĞRU kart (paylaşılan-Esas'ta yanlış-kart edinimini önler)
172+
c1 = find_target_record_card(html, "2026/12", target_record_ref="16760703981")
173+
assert c1 is not None and "record_ref" in c1["match_fields"]
174+
assert "16760703981" in c1["html"] and "16760703976" not in c1["html"]
175+
# eşleşmeyen KAYIT NO → yanlış kartı ALMA (dürüstçe None)
176+
assert find_target_record_card(html, "2026/12", target_record_ref="99999999999") is None
177+
178+
152179
def test_generic_page_level_evrak_text_not_treated_as_target():
153180
# Hedef kayıt olmayan sayfada genel 'İhale Evrak Listesi' başlığı → hedef kart YOK
154181
assert find_target_record_card(_listing_page_no_target(), TARGET, INSTITUTION) is None

0 commit comments

Comments
 (0)