perf: optimize reconciliation candidate lookup

This commit is contained in:
plx
2026-08-14 12:56:03 +00:00
parent 89017f71a6
commit 07dc9db198
8 changed files with 373 additions and 31 deletions

View File

@@ -12,6 +12,7 @@ import json
from sqlalchemy import text
from app.company_opportunity_linking import is_public_email_domain
from app.db import engine
from app.reconciliation_service import (
_apply_jasmin_documents_to_opportunity, # noqa: PLC2701 - deliberate operator maintenance helper
@@ -466,7 +467,8 @@ def find_jasmin_document_candidates_for_opportunity(opportunity_id: str, *, limi
fiscal_customer_id = _as_text(opp.get("local_customer_id"))
fiscal_name = _as_text(opp.get("fiscal_customer_name"))
fiscal_email = _as_text(opp.get("fiscal_customer_email") or opp.get("customer_email")).lower()
domain = _email_domain(fiscal_email)
raw_domain = _email_domain(fiscal_email)
domain = "" if is_public_email_domain(raw_domain) else raw_domain
current_docs = conn.execute(text("""
SELECT
@@ -479,8 +481,7 @@ def find_jasmin_document_candidates_for_opportunity(opportunity_id: str, *, limi
ORDER BY document_date DESC NULLS LAST, created_at DESC
"""), {"opportunity_id": opportunity_id}).mappings().all()
rows = conn.execute(text("""
SELECT
candidate_columns = """
ri.id::text,
ri.source_system,
ri.external_type,
@@ -502,52 +503,159 @@ def find_jasmin_document_candidates_for_opportunity(opportunity_id: str, *, limi
CASE
WHEN CAST(:fiscal_customer_id AS TEXT) <> '' AND ri.customer_id::text = CAST(:fiscal_customer_id AS TEXT) THEN 100
WHEN CAST(:fiscal_tax_id AS TEXT) <> '' AND ri.customer_tax_id = CAST(:fiscal_tax_id AS TEXT) THEN 98
WHEN CAST(:fiscal_tax_id AS TEXT) <> '' AND ri.payload::text ILIKE '%' || CAST(:fiscal_tax_id AS TEXT) || '%' THEN 96
{payload_nif_score}
WHEN CAST(:fiscal_email AS TEXT) <> '' AND LOWER(COALESCE(ri.customer_email, '')) = CAST(:fiscal_email AS TEXT) THEN 90
WHEN CAST(:domain AS TEXT) <> '' AND ri.payload::text ILIKE '%' || CAST(:domain AS TEXT) || '%' THEN 78
{payload_domain_score}
WHEN CAST(:fiscal_name AS TEXT) <> '' AND LOWER(COALESCE(ri.customer_name, '')) = LOWER(CAST(:fiscal_name AS TEXT)) THEN 75
ELSE 0
END AS match_score,
CASE
WHEN CAST(:fiscal_customer_id AS TEXT) <> '' AND ri.customer_id::text = CAST(:fiscal_customer_id AS TEXT) THEN 'customer_id'
WHEN CAST(:fiscal_tax_id AS TEXT) <> '' AND ri.customer_tax_id = CAST(:fiscal_tax_id AS TEXT) THEN 'nif'
WHEN CAST(:fiscal_tax_id AS TEXT) <> '' AND ri.payload::text ILIKE '%' || CAST(:fiscal_tax_id AS TEXT) || '%' THEN 'payload_nif'
{payload_nif_reason}
WHEN CAST(:fiscal_email AS TEXT) <> '' AND LOWER(COALESCE(ri.customer_email, '')) = CAST(:fiscal_email AS TEXT) THEN 'email'
WHEN CAST(:domain AS TEXT) <> '' AND ri.payload::text ILIKE '%' || CAST(:domain AS TEXT) || '%' THEN 'domain_payload'
{payload_domain_reason}
WHEN CAST(:fiscal_name AS TEXT) <> '' AND LOWER(COALESCE(ri.customer_name, '')) = LOWER(CAST(:fiscal_name AS TEXT)) THEN 'exact_name'
ELSE 'none'
END AS match_reason
"""
common_from = """
FROM reconciliation_items ri
WHERE ri.source_system = 'jasmin'
AND ri.external_type IN ('jasmin_quotation', 'jasmin_proforma', 'jasmin_invoice')
AND (ri.opportunity_id IS NULL OR ri.opportunity_id = CAST(:opportunity_id AS UUID))
AND NOT (
CAST(:fiscal_tax_id AS TEXT) <> ''
AND COALESCE(ri.customer_tax_id, '') <> ''
AND ri.customer_tax_id <> CAST(:fiscal_tax_id AS TEXT)
)
AND NOT EXISTS (
SELECT 1 FROM commercial_documents cd
WHERE cd.opportunity_id = CAST(:opportunity_id AS UUID)
AND (
(ri.document_number IS NOT NULL AND cd.document_number = ri.document_number)
OR (ri.external_id IS NOT NULL AND cd.external_id = ri.external_id)
)
AND ri.document_number IS NOT NULL
AND cd.document_number = ri.document_number
)
AND (
(CAST(:fiscal_customer_id AS TEXT) <> '' AND ri.customer_id::text = CAST(:fiscal_customer_id AS TEXT))
OR (CAST(:fiscal_tax_id AS TEXT) <> '' AND ri.customer_tax_id = CAST(:fiscal_tax_id AS TEXT))
OR (CAST(:fiscal_tax_id AS TEXT) <> '' AND ri.payload::text ILIKE '%' || CAST(:fiscal_tax_id AS TEXT) || '%')
OR (CAST(:fiscal_email AS TEXT) <> '' AND LOWER(COALESCE(ri.customer_email, '')) = CAST(:fiscal_email AS TEXT))
OR (CAST(:domain AS TEXT) <> '' AND ri.payload::text ILIKE '%' || CAST(:domain AS TEXT) || '%')
OR (CAST(:fiscal_name AS TEXT) <> '' AND LOWER(COALESCE(ri.customer_name, '')) = LOWER(CAST(:fiscal_name AS TEXT)))
AND NOT EXISTS (
SELECT 1 FROM commercial_documents cd
WHERE cd.opportunity_id = CAST(:opportunity_id AS UUID)
AND ri.external_id IS NOT NULL
AND cd.external_id = ri.external_id
)
ORDER BY match_score DESC, ri.document_date DESC NULLS LAST, ri.updated_at DESC
LIMIT :limit
"""), {
"""
params = {
"opportunity_id": opportunity_id,
"fiscal_customer_id": fiscal_customer_id,
"fiscal_tax_id": fiscal_tax_id,
"fiscal_email": fiscal_email,
"domain": domain,
"fiscal_name": fiscal_name,
"limit": int(limit),
}).mappings().all()
"limit": max(1, int(limit)),
}
structured_columns = candidate_columns.format(
payload_nif_score="",
payload_domain_score="",
payload_nif_reason="",
payload_domain_reason="",
)
structured_sql = """
WITH candidate_ids AS (
SELECT ri.id FROM reconciliation_items ri
WHERE CAST(:fiscal_customer_id AS TEXT) <> ''
AND ri.customer_id IS NOT NULL
AND ri.customer_id = CAST(NULLIF(:fiscal_customer_id, '') AS UUID)
UNION
SELECT ri.id FROM reconciliation_items ri
WHERE CAST(:fiscal_tax_id AS TEXT) <> ''
AND ri.customer_tax_id IS NOT NULL AND ri.customer_tax_id <> ''
AND ri.customer_tax_id = CAST(:fiscal_tax_id AS TEXT)
UNION
SELECT ri.id FROM reconciliation_items ri
WHERE CAST(:fiscal_email AS TEXT) <> ''
AND ri.customer_email IS NOT NULL AND ri.customer_email <> ''
AND lower(ri.customer_email) = CAST(:fiscal_email AS TEXT)
UNION
SELECT ri.id FROM reconciliation_items ri
WHERE CAST(:fiscal_name AS TEXT) <> ''
AND ri.customer_name IS NOT NULL AND ri.customer_name <> ''
AND lower(ri.customer_name) = lower(CAST(:fiscal_name AS TEXT))
)
SELECT {columns}
{common_from}
AND ri.id IN (SELECT id FROM candidate_ids)
ORDER BY match_score DESC, ri.document_date DESC NULLS LAST, ri.updated_at DESC
LIMIT :limit
""".format(columns=structured_columns, common_from=common_from)
structured_rows = [dict(row) for row in conn.execute(text(structured_sql), params).mappings().all()]
# Payload NIF has score 96. It cannot displace a full page made only of
# customer-id/NIF matches (100/98), so that is the semantics-preserving
# fast path. Email/name matches still require the legacy pass because a
# payload NIF candidate may outrank them.
strong_page_complete = (
len(structured_rows) >= params["limit"]
and int(structured_rows[params["limit"] - 1].get("match_score") or 0) >= 96
)
if strong_page_complete:
rows = structured_rows[:params["limit"]]
else:
payload_predicates: list[str] = []
payload_nif_score = payload_nif_reason = ""
payload_domain_score = payload_domain_reason = ""
if fiscal_tax_id:
payload_predicates.append(
"ri.payload::text ILIKE '%' || CAST(:fiscal_tax_id AS TEXT) || '%'"
)
payload_nif_score = "WHEN ri.payload::text ILIKE '%' || CAST(:fiscal_tax_id AS TEXT) || '%' THEN 96"
payload_nif_reason = "WHEN ri.payload::text ILIKE '%' || CAST(:fiscal_tax_id AS TEXT) || '%' THEN 'payload_nif'"
if domain:
payload_predicates.append(
"ri.payload::text ILIKE '%' || CAST(:domain AS TEXT) || '%'"
)
payload_domain_score = "WHEN ri.payload::text ILIKE '%' || CAST(:domain AS TEXT) || '%' THEN 78"
payload_domain_reason = "WHEN ri.payload::text ILIKE '%' || CAST(:domain AS TEXT) || '%' THEN 'domain_payload'"
if not payload_predicates:
rows = structured_rows[:params["limit"]]
payload_rows = []
else:
payload_columns = candidate_columns.format(
payload_nif_score=payload_nif_score,
payload_domain_score=payload_domain_score,
payload_nif_reason=payload_nif_reason,
payload_domain_reason=payload_domain_reason,
)
payload_sql = """
SELECT {columns}
{common_from}
AND ({payload_where})
ORDER BY match_score DESC, ri.document_date DESC NULLS LAST, ri.updated_at DESC
LIMIT :limit
""".format(
columns=payload_columns,
common_from=common_from,
payload_where=" OR ".join(f"({predicate})" for predicate in payload_predicates),
)
payload_rows = [dict(row) for row in conn.execute(text(payload_sql), params).mappings().all()]
if payload_predicates:
merged = {str(row.get("id")): row for row in structured_rows}
merged.update({str(row.get("id")): row for row in payload_rows})
rows = sorted(
merged.values(),
key=lambda row: (
int(row.get("match_score") or 0),
str(row.get("document_date") or ""),
str(row.get("updated_at") or ""),
),
reverse=True,
)[:params["limit"]]
if fiscal_tax_id:
rows = [
row for row in rows
if not _as_text(row.get("customer_tax_id"))
or _as_text(row.get("customer_tax_id")) == fiscal_tax_id
]
current_doc_items = [_commercial_doc_as_jasmin_item(dict(row)) for row in current_docs]
current_latest_key = max((_jasmin_item_recency_tuple(item) for item in current_doc_items), default=None)