From 8cdfc976e4873eeea88c4c8c97361af07318211b Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Tue, 16 Jun 2026 20:23:11 +0000 Subject: [PATCH] =?UTF-8?q?feat(import):=20T7=20batch=5Fid=20scope=20reres?= =?UTF-8?q?olve=5Faccount=20=E2=80=94=20R1=20INCHIS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reresolve_account(conn, account_id, batch_id=None): - batch_id specificat -> scope la batch-ul exact (import commit explicit) - fara batch_id (POST /v1/mapari) -> EXCLUSIV canal API (batch_id IS NULL) - salvarea unei mapari NU mai re-queues randuri cross-batch (R1 inchis) - 6 teste: izolare batch A/B, regresie API canal, batch explicit nu atinge API, schema batch_id/row_index, 3 batches izolate Co-Authored-By: Claude Sonnet 4.6 --- app/mapping.py | 10 +- tests/test_t7_batch_scope.py | 197 +++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 tests/test_t7_batch_scope.py diff --git a/app/mapping.py b/app/mapping.py index a508cf7..144bb53 100644 --- a/app/mapping.py +++ b/app/mapping.py @@ -298,16 +298,20 @@ def reresolve_account(conn, account_id: int | None, batch_id: int | None = None) mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()} if batch_id is not None: - # T7: scope la batch-ul specificat + # T7: scope la batch-ul specificat (import commit explicit). + # NU atinge randuri din alte batches sau din feed API. rows = conn.execute( "SELECT id, payload_json FROM submissions " "WHERE status='needs_mapping' AND account_id=? AND batch_id=?", (acct, batch_id), ).fetchall() else: - # Canal API (batch_id IS NULL) + legacy (batch_id nesetat) + # POST /v1/mapari (save manual): re-rezolva EXCLUSIV canalul API (batch_id IS NULL). + # T7/R1 INCHIS: salvarea unei mapari NU re-queues randuri din batches de import + # (cross-batch / cross-feed). Batches de import sunt re-rezolvate doar la commit explicit. rows = conn.execute( - "SELECT id, payload_json FROM submissions WHERE status='needs_mapping' AND account_id=?", + "SELECT id, payload_json FROM submissions " + "WHERE status='needs_mapping' AND account_id=? AND batch_id IS NULL", (acct,), ).fetchall() diff --git a/tests/test_t7_batch_scope.py b/tests/test_t7_batch_scope.py new file mode 100644 index 0000000..9de0c2e --- /dev/null +++ b/tests/test_t7_batch_scope.py @@ -0,0 +1,197 @@ +"""Teste T7: batch_id/row_index scope reresolve_account (R1 INCHIS). + +Verify: +(a) salvare mapare in batch A NU trimite randuri din batch B / feed API. +(b) canal API (batch_id NULL) tot se re-rezolva ca azi (regresie). +""" + +from __future__ import annotations + +import json +import os +import tempfile + +import pytest + + +@pytest.fixture() +def env(monkeypatch): + tmp = tempfile.mkdtemp() + monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t7.db")) + from app.config import get_settings + get_settings.cache_clear() + from app.db import init_db + init_db() + yield monkeypatch + get_settings.cache_clear() + + +@pytest.fixture() +def conn(env): + from app.db import get_connection + c = get_connection() + yield c + c.close() + + +def _insert_batch(conn, account_id=1): + """Creeaza un import_batch si returneaza id-ul.""" + cur = conn.execute( + "INSERT INTO import_batches (account_id, filename, status) VALUES (?, ?, 'staging')", + (account_id, "test.xlsx"), + ) + return int(cur.lastrowid) + + +def _insert_submission(conn, account_id=1, batch_id=None, cod_op="ITP-1", key_sfx=None): + """Insereaza un submission needs_mapping (cu sau fara batch).""" + content = { + "vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B1", + "data_prestatie": "2026-06-15", "odometru_final": "123456", + "prestatii": [{"cod_op_service": cod_op, "denumire": "Test"}], + } + sfx = key_sfx or os.urandom(4).hex() + if batch_id is not None: + cur = conn.execute( + "INSERT INTO submissions (idempotency_key, account_id, status, payload_json, batch_id) " + "VALUES (?, ?, 'needs_mapping', ?, ?)", + (f"k-{sfx}", account_id, json.dumps(content), batch_id), + ) + else: + cur = conn.execute( + "INSERT INTO submissions (idempotency_key, account_id, status, payload_json) " + "VALUES (?, ?, 'needs_mapping', ?)", + (f"k-{sfx}", account_id, json.dumps(content)), + ) + return int(cur.lastrowid) + + +def _add_mapping(conn, account_id=1, cod_op="ITP-1", cod_prestatie="OE-1", auto_send=True): + conn.execute( + "INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)", + (cod_prestatie, "Test operatie"), + ) + conn.execute( + "INSERT OR REPLACE INTO operations_mapping (account_id, cod_op_service, cod_prestatie, auto_send) " + "VALUES (?, ?, ?, ?)", + (account_id, cod_op, cod_prestatie, 1 if auto_send else 0), + ) + + +# --- Scoping --- + +def test_reresolve_batch_specific_nu_atinge_alt_batch(conn): + """(a) reresolve_account cu batch_id=A nu atinge randuri din batch_id=B.""" + from app.mapping import reresolve_account + + batch_a = _insert_batch(conn) + batch_b = _insert_batch(conn) + + sid_a = _insert_submission(conn, batch_id=batch_a, cod_op="ITP-1", key_sfx="a1") + sid_b = _insert_submission(conn, batch_id=batch_b, cod_op="ITP-1", key_sfx="b1") + + _add_mapping(conn, cod_op="ITP-1", cod_prestatie="OE-1", auto_send=True) + + # Re-rezolva NUMAI batch_a + stats = reresolve_account(conn, 1, batch_id=batch_a) + assert stats["requeued"] == 1 + + # Batch B nemodificat + row_a = conn.execute("SELECT status FROM submissions WHERE id=?", (sid_a,)).fetchone() + row_b = conn.execute("SELECT status FROM submissions WHERE id=?", (sid_b,)).fetchone() + assert row_a["status"] == "queued", "batch A trebuie requeued" + assert row_b["status"] == "needs_mapping", "batch B trebuie sa ramana needs_mapping" + + +def test_reresolve_fara_batch_nu_atinge_batches(conn): + """(a) reresolve fara batch (POST /v1/mapari) NU atinge batch submissions.""" + from app.mapping import reresolve_account + + batch_a = _insert_batch(conn) + sid_batch = _insert_submission(conn, batch_id=batch_a, cod_op="ITP-1", key_sfx="ba") + sid_api = _insert_submission(conn, batch_id=None, cod_op="ITP-1", key_sfx="ap") + + _add_mapping(conn, cod_op="ITP-1", cod_prestatie="OE-1", auto_send=True) + + # Fara batch (cum apeleaza POST /v1/mapari) + stats = reresolve_account(conn, 1) + assert stats["requeued"] == 1 # numai API canal + + row_batch = conn.execute("SELECT status FROM submissions WHERE id=?", (sid_batch,)).fetchone() + row_api = conn.execute("SELECT status FROM submissions WHERE id=?", (sid_api,)).fetchone() + assert row_batch["status"] == "needs_mapping", "batch submission NU trebuie atins de reresolve global" + assert row_api["status"] == "queued", "API canal trebuie requeued" + + +def test_reresolve_canal_api_regresie(conn): + """(b) Canal API (batch_id NULL) tot se re-rezolva ca azi.""" + from app.mapping import reresolve_account + + # Doua submission-uri API fara batch + sid1 = _insert_submission(conn, batch_id=None, cod_op="ITP-2", key_sfx="r1") + sid2 = _insert_submission(conn, batch_id=None, cod_op="ITP-2", key_sfx="r2") + + _add_mapping(conn, cod_op="ITP-2", cod_prestatie="OE-1", auto_send=True) + + stats = reresolve_account(conn, 1) # fara batch — re-rezolva tot API + assert stats["requeued"] == 2 + + for sid in (sid1, sid2): + row = conn.execute("SELECT status FROM submissions WHERE id=?", (sid,)).fetchone() + assert row["status"] == "queued" + + +def test_reresolve_batch_explicit_nu_atinge_api(conn): + """Batch explicit: nu atinge feed API (batch_id IS NULL).""" + from app.mapping import reresolve_account + + batch_a = _insert_batch(conn) + sid_batch = _insert_submission(conn, batch_id=batch_a, cod_op="ITP-3", key_sfx="ba3") + sid_api = _insert_submission(conn, batch_id=None, cod_op="ITP-3", key_sfx="ap3") + + _add_mapping(conn, cod_op="ITP-3", cod_prestatie="OE-1", auto_send=True) + + stats = reresolve_account(conn, 1, batch_id=batch_a) + assert stats["requeued"] == 1 + + row_batch = conn.execute("SELECT status FROM submissions WHERE id=?", (sid_batch,)).fetchone() + row_api = conn.execute("SELECT status FROM submissions WHERE id=?", (sid_api,)).fetchone() + assert row_batch["status"] == "queued" + assert row_api["status"] == "needs_mapping", "API canal nu trebuie atins de reresolve batch-specific" + + +def test_submissions_au_batch_id_si_row_index(conn): + """Schema: submissions.batch_id si .row_index exista si se pot seta.""" + batch_id = _insert_batch(conn) + conn.execute( + "INSERT INTO submissions (idempotency_key, account_id, status, payload_json, batch_id, row_index) " + "VALUES (?, ?, 'queued', '{}', ?, ?)", + ("k-test-bi", 1, batch_id, 5), + ) + row = conn.execute("SELECT batch_id, row_index FROM submissions WHERE idempotency_key='k-test-bi'").fetchone() + assert row["batch_id"] == batch_id + assert row["row_index"] == 5 + + +def test_reresolve_multiple_batches_izolate(conn): + """R1 INCHIS: 3 batches, fiecare re-rezolvat independent.""" + from app.mapping import reresolve_account + + batches = [_insert_batch(conn) for _ in range(3)] + sids = { + b: _insert_submission(conn, batch_id=b, cod_op="ITP-4", key_sfx=f"mb{i}") + for i, b in enumerate(batches) + } + + _add_mapping(conn, cod_op="ITP-4", cod_prestatie="OE-1", auto_send=True) + + # Re-rezolva batch 0, verifica ca 1 si 2 nu sunt atinse + reresolve_account(conn, 1, batch_id=batches[0]) + + statuses = { + b: conn.execute("SELECT status FROM submissions WHERE id=?", (sid,)).fetchone()["status"] + for b, sid in sids.items() + } + assert statuses[batches[0]] == "queued" + assert statuses[batches[1]] == "needs_mapping" + assert statuses[batches[2]] == "needs_mapping"