perf: optimize reconciliation candidate lookup
This commit is contained in:
216
tests/test_reconciliation_candidate_lookup_performance.py
Normal file
216
tests/test_reconciliation_candidate_lookup_performance.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from contextlib import nullcontext
|
||||
from inspect import getsource
|
||||
from pathlib import Path
|
||||
|
||||
import app.commercial_service as commercial_service
|
||||
import app.jasmin_backfill_service as service
|
||||
import app.operation_service as operation_service
|
||||
import app.opportunity_service as opportunity_service
|
||||
import app.product_service as product_service
|
||||
|
||||
|
||||
class _Result:
|
||||
def __init__(self, rows=()):
|
||||
self.rows = list(rows)
|
||||
|
||||
def mappings(self):
|
||||
return self
|
||||
|
||||
def all(self):
|
||||
return self.rows
|
||||
|
||||
def first(self):
|
||||
return self.rows[0] if self.rows else None
|
||||
|
||||
|
||||
class _Connection:
|
||||
def __init__(self, structured_rows, payload_rows=(), *, fiscal_email="buyer@example.test", fiscal_tax_id="501000000"):
|
||||
self.structured_rows = structured_rows
|
||||
self.payload_rows = payload_rows
|
||||
self.fiscal_email = fiscal_email
|
||||
self.fiscal_tax_id = fiscal_tax_id
|
||||
self.statements = []
|
||||
|
||||
def execute(self, statement, params=None):
|
||||
sql = str(statement)
|
||||
self.statements.append(sql)
|
||||
if "FROM opportunities o" in sql:
|
||||
return _Result([{
|
||||
"id": "11111111-1111-1111-1111-111111111111",
|
||||
"customer_name": "Example",
|
||||
"customer_email": "buyer@example.test",
|
||||
"local_customer_id": "22222222-2222-2222-2222-222222222222",
|
||||
"fiscal_customer_name": "Example Lda",
|
||||
"fiscal_customer_tax_id": self.fiscal_tax_id,
|
||||
"fiscal_customer_email": self.fiscal_email,
|
||||
}])
|
||||
if "FROM commercial_documents" in sql and "SELECT\n id::text" in sql:
|
||||
return _Result()
|
||||
if "WITH candidate_ids AS" in sql:
|
||||
return _Result(self.structured_rows)
|
||||
if "ri.payload::text ILIKE" in sql:
|
||||
return _Result(self.payload_rows)
|
||||
raise AssertionError(sql)
|
||||
|
||||
|
||||
class _Engine:
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
def begin(self):
|
||||
return nullcontext(self.connection)
|
||||
|
||||
|
||||
def _candidate(candidate_id, score, reason, *, document_date="2026-08-01"):
|
||||
return {
|
||||
"id": candidate_id,
|
||||
"source_system": "jasmin",
|
||||
"external_type": "jasmin_invoice",
|
||||
"external_id": candidate_id,
|
||||
"title": candidate_id,
|
||||
"status": "open",
|
||||
"opportunity_id": None,
|
||||
"customer_id": None,
|
||||
"customer_name": "Example Lda",
|
||||
"customer_email": "buyer@example.test",
|
||||
"customer_tax_id": "501000000",
|
||||
"document_number": candidate_id,
|
||||
"document_date": document_date,
|
||||
"amount": 10,
|
||||
"currency": "EUR",
|
||||
"payload": {},
|
||||
"created_at": "2026-08-01",
|
||||
"updated_at": "2026-08-02",
|
||||
"match_score": score,
|
||||
"match_reason": reason,
|
||||
}
|
||||
|
||||
|
||||
def test_structured_full_page_skips_payload_statement(monkeypatch):
|
||||
structured = [_candidate("customer", 100, "customer_id"), _candidate("nif", 98, "nif")]
|
||||
connection = _Connection(structured)
|
||||
monkeypatch.setattr(service, "engine", _Engine(connection))
|
||||
|
||||
result = service.find_jasmin_document_candidates_for_opportunity(
|
||||
"11111111-1111-1111-1111-111111111111", limit=2,
|
||||
)
|
||||
|
||||
assert [row["match_reason"] for row in result] == ["customer_id", "nif"]
|
||||
assert len(connection.statements) == 3 # opportunity, current docs, structured
|
||||
assert sum("payload::text ILIKE" in sql for sql in connection.statements) == 0
|
||||
|
||||
|
||||
def test_payload_legacy_fallback_preserves_precedence_and_order(monkeypatch):
|
||||
structured = [_candidate("email", 90, "email", document_date="2026-08-10")]
|
||||
payload = [
|
||||
_candidate("payload-nif", 96, "payload_nif", document_date="2026-07-01"),
|
||||
_candidate("payload-domain", 78, "domain_payload", document_date="2026-08-12"),
|
||||
]
|
||||
connection = _Connection(structured, payload)
|
||||
monkeypatch.setattr(service, "engine", _Engine(connection))
|
||||
|
||||
result = service.find_jasmin_document_candidates_for_opportunity(
|
||||
"11111111-1111-1111-1111-111111111111", limit=5,
|
||||
)
|
||||
|
||||
assert {row["match_reason"] for row in result} == {"payload_nif", "email", "domain_payload"}
|
||||
assert len(connection.statements) == 4
|
||||
assert sum("payload::text ILIKE" in sql for sql in connection.statements) == 1
|
||||
|
||||
|
||||
def test_public_domain_never_adds_domain_payload_predicate(monkeypatch):
|
||||
connection = _Connection([], [_candidate("payload-nif", 96, "payload_nif")], fiscal_email="geral.am.energy@gmail.com")
|
||||
monkeypatch.setattr(service, "engine", _Engine(connection))
|
||||
|
||||
result = service.find_jasmin_document_candidates_for_opportunity(
|
||||
"11111111-1111-1111-1111-111111111111", limit=8,
|
||||
)
|
||||
|
||||
assert [row["match_reason"] for row in result] == ["payload_nif"]
|
||||
payload_sql = connection.statements[-1]
|
||||
assert "CAST(:domain AS TEXT)" not in payload_sql
|
||||
assert "domain_payload" not in payload_sql
|
||||
|
||||
|
||||
def test_public_domain_without_tax_has_no_payload_statement(monkeypatch):
|
||||
for domain in ("gmail.com", "outlook.com", "hotmail.com", "yahoo.com"):
|
||||
connection = _Connection([], fiscal_email=f"person@{domain}", fiscal_tax_id="")
|
||||
monkeypatch.setattr(service, "engine", _Engine(connection))
|
||||
|
||||
assert service.find_jasmin_document_candidates_for_opportunity(
|
||||
"11111111-1111-1111-1111-111111111111", limit=8,
|
||||
) == []
|
||||
assert len(connection.statements) == 3
|
||||
assert all("payload::text ILIKE" not in sql for sql in connection.statements)
|
||||
|
||||
|
||||
def test_private_business_domain_keeps_domain_payload(monkeypatch):
|
||||
candidate = _candidate("private-domain", 78, "domain_payload")
|
||||
candidate["customer_tax_id"] = ""
|
||||
connection = _Connection([], [candidate], fiscal_email="sales@private-business.pt", fiscal_tax_id="")
|
||||
monkeypatch.setattr(service, "engine", _Engine(connection))
|
||||
|
||||
result = service.find_jasmin_document_candidates_for_opportunity(
|
||||
"11111111-1111-1111-1111-111111111111", limit=8,
|
||||
)
|
||||
|
||||
assert [row["match_reason"] for row in result] == ["domain_payload"]
|
||||
assert "CAST(:domain AS TEXT)" in connection.statements[-1]
|
||||
|
||||
|
||||
def test_explicit_conflicting_candidate_nif_is_not_actionable(monkeypatch):
|
||||
conflict = _candidate("conflict", 90, "email")
|
||||
conflict["customer_tax_id"] = "999999999"
|
||||
connection = _Connection([conflict])
|
||||
monkeypatch.setattr(service, "engine", _Engine(connection))
|
||||
|
||||
assert service.find_jasmin_document_candidates_for_opportunity(
|
||||
"11111111-1111-1111-1111-111111111111", limit=8,
|
||||
) == []
|
||||
|
||||
|
||||
def test_required_public_domain_provider_policy():
|
||||
for domain in (
|
||||
"gmail.com", "googlemail.com", "hotmail.com", "outlook.com", "live.com",
|
||||
"yahoo.com", "icloud.com", "me.com", "sapo.pt",
|
||||
):
|
||||
assert service.is_public_email_domain(domain)
|
||||
|
||||
|
||||
def test_candidate_sql_preserves_exclusion_schema_and_ordering():
|
||||
source = getsource(service.find_jasmin_document_candidates_for_opportunity)
|
||||
assert source.count("NOT EXISTS") >= 2
|
||||
assert "cd.document_number = ri.document_number" in source
|
||||
assert "cd.external_id = ri.external_id" in source
|
||||
assert "ORDER BY match_score DESC, ri.document_date DESC NULLS LAST, ri.updated_at DESC" in source
|
||||
for reason in ("customer_id", "nif", "payload_nif", "email", "domain_payload", "exact_name"):
|
||||
assert reason in source
|
||||
|
||||
|
||||
def test_opportunity_detail_read_helpers_do_not_initialize_schema():
|
||||
readers = (
|
||||
opportunity_service.get_opportunity,
|
||||
opportunity_service.list_opportunity_tasks,
|
||||
opportunity_service.list_opportunity_events,
|
||||
product_service.list_products,
|
||||
product_service.list_opportunity_items,
|
||||
commercial_service.list_commercial_documents,
|
||||
commercial_service.get_customer_for_opportunity,
|
||||
operation_service.get_operation_links,
|
||||
)
|
||||
for reader in readers:
|
||||
assert "ensure_" not in getsource(reader), reader.__name__
|
||||
|
||||
|
||||
def test_only_plan_specific_indexes_are_migrated():
|
||||
migration = Path("migrations/009_reconciliation_candidate_lookup_indexes.sql").read_text()
|
||||
for expression in (
|
||||
"reconciliation_items(customer_id)",
|
||||
"reconciliation_items(lower(customer_email))",
|
||||
"reconciliation_items(lower(customer_name))",
|
||||
"commercial_documents(opportunity_id, document_number)",
|
||||
"commercial_documents(opportunity_id, external_id)",
|
||||
):
|
||||
assert expression in migration
|
||||
assert "customer_tax_id" not in migration
|
||||
assert "reconciliation_items(opportunity_id)" not in migration
|
||||
Reference in New Issue
Block a user