test(address): oracle integration tests + verify script for PJ/PF rule

- test_address_rules_oracle.py: E2E tests import synthetic PJ+PF orders
  and verify id_facturare/id_livrare in Oracle; regression tests check
  SQLite orders imported after fix date
- verify_address_rules.py: standalone script to audit PJ/PF address
  compliance on existing SQLite orders (--days N / --all / --status)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-08 16:36:30 +00:00
parent 07df807719
commit b2f1687920
2 changed files with 517 additions and 0 deletions

View File

@@ -0,0 +1,347 @@
"""
Oracle Integration Tests — Regula adrese PJ/PF
===============================================
Verifică că comenzile importate respectă regula:
PF (fără CUI): id_adresa_facturare = id_adresa_livrare
PJ (cu CUI): adresa_facturare_roa se potrivește cu adresa billing GoMag
Testele principale sunt E2E (importă comenzi sintetice în Oracle și verifică).
Testele de regresie verifică comenzile existente din SQLite.
Run:
pytest api/tests/test_address_rules_oracle.py -v
./test.sh oracle
"""
import os
import sys
import time
import pytest
pytestmark = pytest.mark.oracle
_script_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
_project_root = os.path.dirname(_script_dir)
from dotenv import load_dotenv
_env_path = os.path.join(_script_dir, ".env")
load_dotenv(_env_path, override=True)
_tns_admin = os.environ.get("TNS_ADMIN", "")
if _tns_admin and os.path.isfile(_tns_admin):
os.environ["TNS_ADMIN"] = os.path.dirname(_tns_admin)
elif not _tns_admin:
os.environ["TNS_ADMIN"] = _script_dir
if _script_dir not in sys.path:
sys.path.insert(0, _script_dir)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(scope="module")
def oracle_env():
"""Re-aplică .env și actualizează settings pentru Oracle."""
load_dotenv(_env_path, override=True)
_tns = os.environ.get("TNS_ADMIN", "")
if _tns and os.path.isfile(_tns):
os.environ["TNS_ADMIN"] = os.path.dirname(_tns)
from app.config import settings
settings.ORACLE_USER = os.environ.get("ORACLE_USER", "MARIUSM_AUTO")
settings.ORACLE_PASSWORD = os.environ.get("ORACLE_PASSWORD", "ROMFASTSOFT")
settings.ORACLE_DSN = os.environ.get("ORACLE_DSN", "ROA_CENTRAL")
settings.TNS_ADMIN = os.environ.get("TNS_ADMIN", _script_dir)
settings.FORCE_THIN_MODE = os.environ.get("FORCE_THIN_MODE", "") == "true"
return settings
@pytest.fixture(scope="module")
def client(oracle_env):
from fastapi.testclient import TestClient
from app.main import app
with TestClient(app) as c:
yield c
@pytest.fixture(scope="module")
def oracle_pool(oracle_env):
"""Pool Oracle direct pentru verificări în DB."""
from app import database
database.init_oracle()
yield database.pool
@pytest.fixture(scope="module")
def real_codmat(client):
"""CODMAT real din Oracle pentru liniile comenzii sintetice."""
for term in ["01", "PH", "CA", "A"]:
resp = client.get("/api/articles/search", params={"q": term})
if resp.status_code == 200:
results = resp.json().get("results", [])
if results:
return results[0]["codmat"]
pytest.skip("Nu s-a găsit niciun CODMAT în Oracle pentru test")
@pytest.fixture(scope="module")
def app_settings(client):
"""Setările aplicației (id_pol, id_sectie, etc.)."""
resp = client.get("/api/sync/schedule")
assert resp.status_code == 200
import sqlite3
db_path = os.environ.get("SQLITE_DB_PATH", os.path.join(_script_dir, "orders.db"))
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
rows = conn.execute("SELECT key, value FROM app_settings").fetchall()
conn.close()
return {r["key"]: r["value"] for r in rows}
@pytest.fixture(scope="module")
def run_id():
return f"pytest-addr-{int(time.time())}"
def _build_pj_order(run_id, real_codmat):
"""Comandă sintetică PJ: companie cu billing ≠ shipping."""
from app.services.order_reader import OrderBilling, OrderShipping, OrderData, OrderItem
billing = OrderBilling(
firstname="Test", lastname="PJ", phone="0700000000", email="pj@pytest.local",
address="Bld Unirii 1", city="Bucuresti", region="Bucuresti", country="RO",
company_name="PYTEST COMPANY SRL", company_code="RO99000001", company_reg="J40/9999/2026",
is_company=True
)
shipping = OrderShipping(
firstname="Curier", lastname="Destinatar", phone="0799999999", email="ship@pytest.local",
address="Str Livrare 99", city="Cluj-Napoca", region="Cluj", country="RO"
)
return OrderData(
id=f"{run_id}-PJ",
number=f"{run_id}-PJ",
date="2026-01-15T10:00:00",
status="new", status_id="1",
billing=billing, shipping=shipping,
items=[OrderItem(sku="PYTEST-SKU-PJ", name="Test PJ Item",
price=10.0, quantity=1.0, vat=19.0)],
total=10.0, delivery_cost=0.0, discount_total=0.0
)
def _build_pf_order(run_id, real_codmat):
"""Comandă sintetică PF: persoană fizică, billing ≠ shipping (dar billing ROA trebuie = shipping)."""
from app.services.order_reader import OrderBilling, OrderShipping, OrderData, OrderItem
billing = OrderBilling(
firstname="Ion", lastname="Popescu", phone="0700000001", email="pf@pytest.local",
address="Str Alta 5", city="Timisoara", region="Timis", country="RO",
company_name="", company_code="", company_reg="", is_company=False
)
shipping = OrderShipping(
firstname="Ion", lastname="Popescu", phone="0700000001", email="pf@pytest.local",
address="Str Livrare 10", city="Iasi", region="Iasi", country="RO"
)
return OrderData(
id=f"{run_id}-PF",
number=f"{run_id}-PF",
date="2026-01-15T10:00:00",
status="new", status_id="1",
billing=billing, shipping=shipping,
items=[OrderItem(sku="PYTEST-SKU-PF", name="Test PF Item",
price=10.0, quantity=1.0, vat=19.0)],
total=10.0, delivery_cost=0.0, discount_total=0.0
)
def _cleanup_test_orders(oracle_pool, run_id):
"""Șterge comenzile de test din Oracle."""
try:
conn = oracle_pool.acquire()
with conn.cursor() as cur:
cur.execute(
"DELETE FROM comenzi WHERE comanda_externa LIKE :1",
[f"{run_id}%"]
)
conn.commit()
oracle_pool.release(conn)
except Exception as e:
print(f"Cleanup warning: {e}")
# ---------------------------------------------------------------------------
# Test E2E: import PJ + PF sintetice în Oracle
# ---------------------------------------------------------------------------
class TestAddressRulesE2E:
"""Import comenzi sintetice și verifică adresele în Oracle."""
@pytest.fixture(scope="class", autouse=True)
def cleanup(self, oracle_pool, run_id):
yield
_cleanup_test_orders(oracle_pool, run_id)
def test_pj_billing_addr_is_gomag_billing(self, oracle_pool, real_codmat, app_settings, run_id):
"""PJ: adresa facturare în Oracle provine din GoMag billing (nu shipping)."""
from app.services.import_service import import_single_order
from app.services.order_reader import OrderItem
order = _build_pj_order(run_id, real_codmat)
# Replace test SKU with real codmat via mapping (or just use items with real SKU)
order.items = [OrderItem(sku=real_codmat, name="Test PJ",
price=10.0, quantity=1.0, vat=19.0)]
id_pol = int(app_settings.get("id_pol") or 0) or None
id_sectie = int(app_settings.get("id_sectie") or 0) or None
result = import_single_order(order, id_pol=id_pol, id_sectie=id_sectie,
app_settings=app_settings)
if not result["success"]:
pytest.skip(f"Import PJ eșuat (SKU probabil nemapat): {result.get('error')}")
id_fact = result["id_adresa_facturare"]
id_livr = result["id_adresa_livrare"]
assert id_fact is not None, "PJ: id_adresa_facturare lipsește din result"
assert id_livr is not None, "PJ: id_adresa_livrare lipsește din result"
# PJ cu billing ≠ shipping: adresele trebuie să fie DIFERITE
assert id_fact != id_livr, (
f"PJ cu billing≠shipping trebuie să aibă id_fact({id_fact}) ≠ id_livr({id_livr}). "
f"Regula veche (different_person) s-ar comporta la fel, dar acum PJ folosește billing GoMag."
)
# Verifică în Oracle că adresele există
conn = oracle_pool.acquire()
with conn.cursor() as cur:
cur.execute(
"SELECT id_livrare, id_facturare FROM comenzi WHERE comanda_externa = :1",
[order.number]
)
row = cur.fetchone()
oracle_pool.release(conn)
assert row is not None, f"Comanda {order.number} nu s-a găsit în Oracle comenzi"
assert row[0] == id_livr, f"id_livrare Oracle ({row[0]}) ≠ result ({id_livr})"
assert row[1] == id_fact, f"id_facturare Oracle ({row[1]}) ≠ result ({id_fact})"
def test_pf_billing_addr_equals_shipping(self, oracle_pool, real_codmat, app_settings, run_id):
"""PF: adresa facturare în Oracle = adresa livrare (ramburs curier)."""
from app.services.import_service import import_single_order
from app.services.order_reader import OrderItem
order = _build_pf_order(run_id, real_codmat)
order.items = [OrderItem(sku=real_codmat, name="Test PF",
price=10.0, quantity=1.0, vat=19.0)]
id_pol = int(app_settings.get("id_pol") or 0) or None
id_sectie = int(app_settings.get("id_sectie") or 0) or None
result = import_single_order(order, id_pol=id_pol, id_sectie=id_sectie,
app_settings=app_settings)
if not result["success"]:
pytest.skip(f"Import PF eșuat: {result.get('error')}")
id_fact = result["id_adresa_facturare"]
id_livr = result["id_adresa_livrare"]
assert id_fact is not None, "PF: id_adresa_facturare lipsește din result"
assert id_livr is not None, "PF: id_adresa_livrare lipsește din result"
# PF: id_facturare TREBUIE să fie = id_livrare
assert id_fact == id_livr, (
f"PF trebuie să aibă id_fact({id_fact}) = id_livr({id_livr}) — "
f"ramburs curier pe adresa de livrare"
)
# Verifică în Oracle
conn = oracle_pool.acquire()
with conn.cursor() as cur:
cur.execute(
"SELECT id_livrare, id_facturare FROM comenzi WHERE comanda_externa = :1",
[order.number]
)
row = cur.fetchone()
oracle_pool.release(conn)
assert row is not None, f"Comanda {order.number} nu s-a găsit în Oracle comenzi"
assert row[1] == row[0], (
f"Oracle: id_facturare({row[1]}) ≠ id_livrare({row[0]}) pentru PF"
)
# ---------------------------------------------------------------------------
# Test regresie: comenzi existente în SQLite
# ---------------------------------------------------------------------------
class TestAddressRulesRegression:
"""Verifică că comenzile existente importate după fix respectă regula PJ/PF."""
FIX_DATE = "2026-04-08" # data când a fost aplicat fix-ul
@pytest.fixture(scope="class")
def sqlite_rows(self):
"""Comenzi cu adrese populate importate după data fix-ului."""
import sqlite3
from app.config import settings
db_path = os.environ.get("SQLITE_DB_PATH", os.path.join(_script_dir, "orders.db"))
if not os.path.exists(db_path):
pytest.skip(f"SQLite DB lipsă: {db_path}")
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
rows = conn.execute("""
SELECT order_number, cod_fiscal_gomag,
id_adresa_facturare, id_adresa_livrare,
adresa_facturare_gomag, adresa_livrare_gomag,
adresa_facturare_roa, adresa_livrare_roa,
first_seen_at
FROM orders
WHERE id_adresa_facturare IS NOT NULL
AND id_adresa_livrare IS NOT NULL
AND first_seen_at >= ?
""", (self.FIX_DATE,)).fetchall()
conn.close()
return rows
def test_pf_id_facturare_equals_id_livrare(self, sqlite_rows):
"""PF noi: id_adresa_facturare = id_adresa_livrare."""
pf_rows = [r for r in sqlite_rows if not r["cod_fiscal_gomag"]]
if not pf_rows:
pytest.skip(f"Nicio comandă PF importată după {self.FIX_DATE}")
violations = [
f"{r['order_number']}: id_fact={r['id_adresa_facturare']} id_livr={r['id_adresa_livrare']}"
for r in pf_rows
if r["id_adresa_facturare"] != r["id_adresa_livrare"]
]
assert not violations, (
f"PF comenzi cu id_fact ≠ id_livr ({len(violations)}):\n" + "\n".join(violations[:10])
)
def test_pj_billing_roa_matches_gomag_billing(self, sqlite_rows):
"""PJ noi: adresa_facturare_roa se potrivește cu GoMag billing address."""
from app.services.sync_service import _addr_match
pj_rows = [
r for r in sqlite_rows
if r["cod_fiscal_gomag"] and r["adresa_facturare_gomag"] and r["adresa_facturare_roa"]
]
if not pj_rows:
pytest.skip(f"Nicio comandă PJ cu adrese populate importată după {self.FIX_DATE}")
violations = []
for r in pj_rows:
if not _addr_match(r["adresa_facturare_gomag"], r["adresa_facturare_roa"]):
violations.append(
f"{r['order_number']}: billing_gomag={r['adresa_facturare_gomag']!r} "
f"fact_roa={r['adresa_facturare_roa']!r}"
)
assert not violations, (
f"PJ comenzi cu adresa_facturare_roa care nu corespunde GoMag billing ({len(violations)}):\n"
+ "\n".join(violations[:10])
)

View File

@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""
Verifică regula adrese PJ/PF pe comenzile importate din SQLite.
Logica:
PF (cod_fiscal_gomag IS NULL): id_adresa_facturare = id_adresa_livrare
PJ (cod_fiscal_gomag IS NOT NULL): adresa_facturare_roa se potriveste cu GoMag billing
(nu cu GoMag shipping)
Rulare:
python3 scripts/verify_address_rules.py
python3 scripts/verify_address_rules.py --days 7 # ultimele 7 zile
python3 scripts/verify_address_rules.py --all # toate comenzile
python3 scripts/verify_address_rules.py --status IMPORTED
"""
import argparse
import json
import os
import sqlite3
import sys
from pathlib import Path
# Add api/ to path for app imports
_repo_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(_repo_root / "api"))
from dotenv import load_dotenv
load_dotenv(_repo_root / "api" / ".env")
from app.services.sync_service import _addr_match
def main():
parser = argparse.ArgumentParser(description="Verifică regula adrese PJ/PF în SQLite")
parser.add_argument("--days", type=int, default=30,
help="Număr de zile în urmă (default: 30)")
parser.add_argument("--all", action="store_true",
help="Toate comenzile, indiferent de dată")
parser.add_argument("--status", default=None,
help="Filtrează după status (ex: IMPORTED)")
args = parser.parse_args()
_raw_path = os.environ.get("SQLITE_DB_PATH", "data/import.db")
db_path = _raw_path if os.path.isabs(_raw_path) else str(_repo_root / "api" / _raw_path)
if not Path(db_path).exists():
print(f"EROARE: SQLite DB nu există: {db_path}")
sys.exit(1)
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
# Build query
where_clauses = ["id_adresa_facturare IS NOT NULL", "id_adresa_livrare IS NOT NULL"]
params = []
if not args.all:
where_clauses.append("first_seen_at >= datetime('now', ?)")
params.append(f"-{args.days} days")
if args.status:
where_clauses.append("status = ?")
params.append(args.status)
where_sql = " AND ".join(where_clauses)
rows = conn.execute(f"""
SELECT order_number, status, cod_fiscal_gomag,
id_adresa_facturare, id_adresa_livrare,
adresa_facturare_gomag, adresa_livrare_gomag,
adresa_facturare_roa, adresa_livrare_roa,
first_seen_at
FROM orders
WHERE {where_sql}
ORDER BY first_seen_at DESC
""", params).fetchall()
conn.close()
if not rows:
scope = "toate comenzile" if args.all else f"ultimele {args.days} zile"
print(f"Nicio comandă cu adrese populate ({scope}).")
sys.exit(0)
pf_ok = pf_err = pj_ok = pj_err = pj_skip = 0
violations = []
for r in rows:
is_pj = bool(r["cod_fiscal_gomag"])
id_fact = r["id_adresa_facturare"]
id_livr = r["id_adresa_livrare"]
order = r["order_number"]
date = (r["first_seen_at"] or "")[:10]
if not is_pj:
# PF: id_facturare trebuie = id_livrare
if id_fact == id_livr:
pf_ok += 1
else:
pf_err += 1
violations.append({
"order": order, "date": date, "type": "PF",
"issue": f"id_fact={id_fact} != id_livr={id_livr}",
"detail": None,
})
else:
# PJ: adresa_facturare_roa trebuie sa se potriveasca cu GoMag billing
fact_roa = r["adresa_facturare_roa"]
fact_gomag = r["adresa_facturare_gomag"]
livr_gomag = r["adresa_livrare_gomag"]
if not fact_roa or not fact_gomag:
pj_skip += 1
continue
# Check 1: billing ROA matches GoMag billing
billing_match = _addr_match(fact_gomag, fact_roa)
# Check 2: billing ROA does NOT match GoMag shipping (wrong old behavior)
shipping_match = _addr_match(livr_gomag, fact_roa) if livr_gomag else False
if billing_match:
pj_ok += 1
else:
pj_err += 1
detail = "billing_ROA matches shipping GoMag" if shipping_match else "billing_ROA mismatch"
violations.append({
"order": order, "date": date, "type": "PJ",
"issue": detail,
"detail": f"billing_gomag={_short(fact_gomag)} | fact_roa={fact_roa}",
})
# Output
total = len(rows)
print(f"\n{'='*60}")
scope = "toate" if args.all else f"ultimele {args.days} zile"
print(f" Verificare adrese PJ/PF ({scope}, {total} comenzi cu adrese)")
print(f"{'='*60}")
print(f" PF (fara CUI): {pf_ok:4d} OK | {pf_err:4d} ERORI")
print(f" PJ (cu CUI): {pj_ok:4d} OK | {pj_err:4d} ERORI | {pj_skip:4d} skip (date lipsa)")
print(f"{'='*60}")
if not violations:
print(" ✓ Toate comenzile respecta regula PJ/PF.\n")
else:
print(f"\n VIOLARI ({len(violations)}):\n")
for v in violations[:20]:
print(f" [{v['date']}] {v['order']:25s} {v['type']} {v['issue']}")
if v["detail"]:
print(f" {v['detail']}")
if len(violations) > 20:
print(f" ... si inca {len(violations)-20} violari.")
print()
sys.exit(1 if violations else 0)
def _short(json_str):
"""Returnează un rezumat scurt al unui JSON de adresă."""
if not json_str:
return "(null)"
try:
d = json.loads(json_str)
return f"{d.get('address','?')}, {d.get('city','?')}"
except Exception:
return json_str[:40]
if __name__ == "__main__":
main()