  SELECT t.transaction_id,
    d_sub.document_id,
    'window_only' AS seed
  FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s4_merchant_id_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s5_merchant_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s6_customer_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s7_iban_plus_signal
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s8_merchant_id_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s9_merchant_ns_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s10_customer_ns_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s11_iban_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s12_window_amount_exact
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s13_window_amount_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
      ORDER BY d.document_date_unix,
        d.document_id
      LIMIT v_cap_window_only
    ) d_sub ON TRUE
  WHERE done.transaction_id IS NULL
),
seeds AS (
  SELECT *
  FROM s1_invoice
  UNION ALL
  SELECT *
  FROM s2_order
  UNION ALL
  SELECT *
  FROM s3_iban_exact_amount
  UNION ALL
  SELECT *
  FROM s4_merchant_id_exact_amount
  UNION ALL
  SELECT *
  FROM s5_merchant_ns_exact_amount
  UNION ALL
  SELECT *
  FROM s6_customer_ns_exact_amount
  UNION ALL
  SELECT *
  FROM s7_iban_plus_signal
  UNION ALL
  SELECT *
  FROM s8_merchant_id_tol
  UNION ALL
  SELECT *
  FROM s9_merchant_ns_tol
  UNION ALL
  SELECT *
  FROM s10_customer_ns_tol
  UNION ALL
  SELECT *
  FROM s11_iban_tol
  UNION ALL
  SELECT *
  FROM s12_window_amount_exact
  UNION ALL
  SELECT *
  FROM s13_window_amount_tol
  UNION ALL
  SELECT *
  FROM s14_window_only
),
seed_counts_total AS (
  SELECT seed,
    COUNT(*) AS ct
  FROM seeds
  GROUP BY seed
),
seed_counts_distinct AS (
  SELECT seed,
    COUNT(DISTINCT (transaction_id, document_id)) AS ct
  FROM seeds
  GROUP BY seed
),
candidate_pairs AS (
  SELECT DISTINCT transaction_id,
    document_id
  FROM seeds
),
/* ---------------- scoring ---------------- */
distinct_pairs AS (
  SELECT DISTINCT transaction_id,
    document_id
  FROM candidate_pairs
),
attributes AS (
  SELECT p.transaction_id,
    p.document_id,
    t.company_id::uuid AS transaction_company_id,
    d.company_id AS document_company_id,
    t.amount AS transaction_amount,
    t.debited AS transaction_debited,
    t.credited AS transaction_credited,
    d.total AS document_total,
    t.currency AS transaction_currency,
    d.currency AS document_currency,
    t.invoice_nr_norm AS tx_invoice_norm,
    d.invoice_number_norm AS doc_invoice_norm,
    t.order_id_norm AS tx_order_norm,
    d.order_id_norm AS doc_order_norm,
    d.invoice_number_raw AS doc_invoice_raw,
    d.order_id_raw AS doc_order_raw,
    t.completed_date AS transaction_date_unix,
    d.document_date_unix AS document_date_unix,
    t.merchant_id AS transaction_merchant_id,
    d.merchant_id AS document_merchant_id,
    t.tx_customer_display AS transaction_customer_name,
    d.doc_customer_display AS document_customer_name,
    t.tx_is_in,
    t.tx_is_out,
    t.tx_merchant_ns,
    d.doc_merchant_ns,
    t.tx_customer_ns,
    d.doc_customer_ns,
    t.tx_iban_beneficiary_ns,
    t.tx_iban_counterparty_ns,
    d.doc_iban_ns,
    t.tx_iban_beneficiary_core_ns,
    t.tx_iban_counterparty_core_ns,
    d.doc_iban_core_ns,
    COALESCE(NULLIF(d.doc_dedup_key, ''), d.document_id::text) AS doc_dedup_key,
    t.effective_amount AS transaction_effective_amount
  FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope d ON d.document_id = p.document_id
  WHERE t.company_id::uuid = d.company_id
),
evaluated AS (
  SELECT a.transaction_id,
    a.document_id,
    a.transaction_effective_amount,
    COALESCE(
      (
        a.tx_invoice_norm IS NOT NULL
        AND a.doc_invoice_norm IS NOT NULL
        AND a.tx_invoice_norm = a.doc_invoice_norm
      ),
      FALSE
    ) AS invoice_number_match,
    COALESCE(
      (
        a.tx_order_norm IS NOT NULL
        AND a.doc_order_norm IS NOT NULL
        AND a.tx_order_norm = a.doc_order_norm
      ),
      FALSE
    ) AS order_number_match,
    COALESCE(
      a.transaction_currency = a.document_currency,
      FALSE
    ) AS currency_match,
    (
      CASE
        WHEN v_directional_matching THEN (
          a.tx_is_out
          AND (
            (
              a.transaction_merchant_id IS NOT NULL
              AND a.document_merchant_id IS NOT NULL
              AND a.transaction_merchant_id = a.document_merchant_id
            )
            OR (
              a.tx_merchant_ns IS NOT NULL
              AND a.doc_merchant_ns IS NOT NULL
              AND (
                a.tx_merchant_ns = a.doc_merchant_ns
                OR position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                OR position(a.tx_merchant_ns in a.doc_merchant_ns) > 0
              )
            )
            OR (
              (
                a.tx_iban_counterparty_ns IS NOT NULL
                AND a.doc_iban_ns IS NOT NULL
                AND a.tx_iban_counterparty_ns = a.doc_iban_ns
              )
              OR (
                a.tx_iban_counterparty_core_ns IS NOT NULL
                AND a.doc_iban_core_ns IS NOT NULL
                AND a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns
              )
            )
          )
        )
        ELSE (
          (
            a.transaction_merchant_id IS NOT NULL
            AND a.document_merchant_id IS NOT NULL
            AND a.transaction_merchant_id = a.document_merchant_id
          )
          OR (
            a.tx_merchant_ns IS NOT NULL
            AND a.doc_merchant_ns IS NOT NULL
            AND (
              a.tx_merchant_ns = a.doc_merchant_ns
              OR position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
              OR position(a.tx_merchant_ns in a.doc_merchant_ns) > 0
            )
          )
          OR (
            (
              a.tx_iban_counterparty_ns IS NOT NULL
              AND a.doc_iban_ns IS NOT NULL
              AND a.tx_iban_counterparty_ns = a.doc_iban_ns
            )
            OR (
              a.tx_iban_counterparty_core_ns IS NOT NULL
              AND a.doc_iban_core_ns IS NOT NULL
              AND a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns
            )
          )
        )
      END
    ) AS merchant_match,
    (
      CASE
        WHEN v_directional_matching THEN (
          a.tx_is_in
          AND (
            (
              a.tx_customer_ns IS NOT NULL
              AND a.doc_customer_ns IS NOT NULL
              AND (
                a.tx_customer_ns = a.doc_customer_ns
                OR position(a.doc_customer_ns in a.tx_customer_ns) > 0
                OR position(a.tx_customer_ns in a.doc_customer_ns) > 0
              )
            )
            OR (
              (
                a.tx_iban_beneficiary_ns IS NOT NULL
                AND a.doc_iban_ns IS NOT NULL
                AND a.tx_iban_beneficiary_ns = a.doc_iban_ns
              )
              OR (
                a.tx_iban_beneficiary_core_ns IS NOT NULL
                AND a.doc_iban_core_ns IS NOT NULL
                AND a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns
              )
            )
          )
        )
        ELSE (
          (
            a.tx_customer_ns IS NOT NULL
            AND a.doc_customer_ns IS NOT NULL
            AND (
              a.tx_customer_ns = a.doc_customer_ns
              OR position(a.doc_customer_ns in a.tx_customer_ns) > 0
              OR position(a.tx_customer_ns in a.doc_customer_ns) > 0
            )
          )
          OR (
            (
              a.tx_iban_beneficiary_ns IS NOT NULL
              AND a.doc_iban_ns IS NOT NULL
              AND a.tx_iban_beneficiary_ns = a.doc_iban_ns
            )
            OR (
              a.tx_iban_beneficiary_core_ns IS NOT NULL
              AND a.doc_iban_core_ns IS NOT NULL
              AND a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns
            )
          )
        )
      END
    ) AS customer_match,
    COALESCE(
      ABS(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds,
      FALSE
    ) AS date_within_cap,
    a.document_total,
    a.document_company_id,
    a.transaction_date_unix,
    a.document_date_unix,
    a.doc_dedup_key,
    (
      a.transaction_debited IS NOT NULL
      AND a.transaction_debited > 0
      AND a.document_total = a.transaction_debited
    ) AS amount_exact_from_debited,
    (
      a.transaction_credited IS NOT NULL
      AND a.transaction_credited > 0
      AND a.document_total = a.transaction_credited
    ) AS amount_exact_from_credited,
    (
      a.transaction_debited IS NOT NULL
      AND a.transaction_debited > 0
      AND ABS(a.document_total - a.transaction_debited) <= a.document_total * v_amount_tolerance_fraction
    ) AS amount_tolerance_from_debited,
    (
      a.transaction_credited IS NOT NULL
      AND a.transaction_credited > 0
      AND ABS(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction
    ) AS amount_tolerance_from_credited,
    a.tx_is_in,
    a.tx_is_out,
    -- carry for summary
    a.tx_iban_beneficiary_ns,
    a.tx_iban_counterparty_ns,
    a.doc_iban_ns,
    a.tx_iban_beneficiary_core_ns,
    a.tx_iban_counterparty_core_ns,
    a.doc_iban_core_ns,
    a.tx_merchant_ns,
    a.doc_merchant_ns,
    a.tx_customer_ns,
    a.doc_customer_ns,
    a.transaction_merchant_id,
    a.document_merchant_id
  FROM attributes a
),
with_amount_tolerance AS (
  SELECT e.*,
    CASE
      WHEN e.tx_is_out
      AND e.amount_exact_from_debited THEN TRUE
      WHEN e.tx_is_in
      AND e.amount_exact_from_credited THEN TRUE
      WHEN NOT v_directional_matching
      AND (
        e.amount_exact_from_debited
        OR e.amount_exact_from_credited
      ) THEN TRUE
      ELSE FALSE
    END AS amount_exact_match,
    CASE
      WHEN v_amount_tolerance_fraction > 0 THEN CASE
        WHEN e.tx_is_out
        AND e.amount_tolerance_from_debited THEN TRUE
        WHEN e.tx_is_in
        AND e.amount_tolerance_from_credited THEN TRUE
        WHEN NOT v_directional_matching
        AND (
          e.amount_tolerance_from_debited
          OR e.amount_tolerance_from_credited
        ) THEN TRUE
        ELSE FALSE
      END
      ELSE FALSE
    END AS amount_tolerance_match,
    (
      e.amount_exact_from_debited
      OR e.amount_exact_from_credited
      OR e.amount_tolerance_from_debited
      OR e.amount_tolerance_from_credited
    ) AS amount_direction_ok
  FROM evaluated e
),
resolved_weights AS (
  SELECT COALESCE((v_weights_json->>'amount_total')::numeric, 0) AS amount_weight,
    COALESCE((v_weights_json->>'invoice_number')::numeric, 0) AS invoice_weight,
    COALESCE((v_weights_json->>'order_number')::numeric, 0) AS order_weight,
    COALESCE((v_weights_json->>'date')::numeric, 0) AS date_weight,
    COALESCE((v_weights_json->>'currency')::numeric, 0) AS currency_weight,
    COALESCE((v_weights_json->>'merchant_id')::numeric, 0) AS merchant_weight,
    COALESCE((v_weights_json->>'customer_name')::numeric, 0) AS customer_weight
),
scored_components AS (
  SELECT m.transaction_id,
    m.document_id,
    m.document_company_id,
    m.doc_dedup_key,
    m.transaction_date_unix,
    m.document_date_unix,
    m.amount_direction_ok,
    m.tx_is_in,
    m.tx_is_out,
    -- carry fields for summary
    m.tx_iban_beneficiary_ns,
    m.tx_iban_counterparty_ns,
    m.doc_iban_ns,
    m.tx_iban_beneficiary_core_ns,
    m.tx_iban_counterparty_core_ns,
    m.doc_iban_core_ns,
    m.tx_merchant_ns,
    m.doc_merchant_ns,
    m.tx_customer_ns,
    m.doc_customer_ns,
    m.transaction_merchant_id,
    m.document_merchant_id,
    COALESCE(
      CASE
        WHEN m.amount_exact_match THEN w.amount_weight
        WHEN m.amount_tolerance_match THEN w.amount_weight * EXP(
          -0.5 * POWER(
            (
              ABS(
                m.transaction_effective_amount - m.document_total
              )
            ) / NULLIF(
              (m.document_total * v_amount_tolerance_fraction) / v_gauss_k,
              0
            ),
            2
          )
        )
        ELSE 0
      END,
      0
    )::numeric AS amount_score,
    (
      m.amount_exact_match
      OR m.amount_tolerance_match
    ) AS amount_match,
    m.invoice_number_match AS invoice_number_match,
    m.order_number_match AS order_number_match,
    CASE
      WHEN m.transaction_date_unix IS NOT NULL
      AND m.document_date_unix IS NOT NULL THEN w.date_weight * EXP(
        -0.5 * POWER(
          GREATEST(
            0,
            (
              ABS(m.transaction_date_unix - m.document_date_unix) / 86400.0
            ) - v_date_full_weight_grace_days
          ) / NULLIF(
            GREATEST(
              1.0,
              (
                v_config_date_window_days - v_date_full_weight_grace_days
              ) / v_gauss_k
            ),
            0
          ),
          2
        )
      )
      ELSE 0
    END::numeric AS date_score,
    m.date_within_cap AS date_match,
    m.currency_match AS currency_match,
    m.merchant_match AS merchant_match,
    m.customer_match AS customer_match,
    w.amount_weight AS amount_weight,
    w.invoice_weight AS invoice_weight,
    w.order_weight AS order_weight,
    w.date_weight AS date_weight,
    w.currency_weight AS currency_weight,
    w.merchant_weight AS merchant_weight,
    w.customer_weight AS customer_weight,
    (
      COALESCE(m.invoice_number_match::int, 0) * w.invoice_weight
    )::numeric AS invoice_score,
    (
      COALESCE(m.order_number_match::int, 0) * w.order_weight
    )::numeric AS order_score,
    (
      COALESCE(m.currency_match::int, 0) * w.currency_weight
    )::numeric AS currency_score,
    (
      COALESCE(m.merchant_match::int, 0) * w.merchant_weight
    )::numeric AS merchant_score,
    (
      COALESCE(m.customer_match::int, 0) * w.customer_weight
    )::numeric AS customer_score,
    m.transaction_effective_amount,
    m.document_total
  FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
),
scored_with_applicability AS (
  SELECT s.*,
    (
      CASE
        WHEN v_directional_matching THEN (
          s.tx_is_out
          AND (
            (
              s.doc_merchant_ns IS NOT NULL
              AND s.tx_merchant_ns IS NOT NULL
            )
            OR (
              (
                s.doc_iban_ns IS NOT NULL
                AND s.tx_iban_counterparty_ns IS NOT NULL
                AND s.doc_iban_ns = s.tx_iban_counterparty_ns
              )
              OR (
                s.doc_iban_core_ns IS NOT NULL
                AND s.tx_iban_counterparty_core_ns IS NOT NULL
                AND s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns
              )
            )
            OR (
              s.document_merchant_id IS NOT NULL
              AND s.transaction_merchant_id IS NOT NULL
            )
          )
        )
        ELSE (
          (
            s.doc_merchant_ns IS NOT NULL
            AND s.tx_merchant_ns IS NOT NULL
          )
          OR (
            (
              s.doc_iban_ns IS NOT NULL
              AND s.tx_iban_counterparty_ns IS NOT NULL
              AND s.doc_iban_ns = s.tx_iban_counterparty_ns
            )
            OR (
              s.doc_iban_core_ns IS NOT NULL
              AND s.tx_iban_counterparty_core_ns IS NOT NULL
              AND s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns
            )
          )
          OR (
            s.document_merchant_id IS NOT NULL
            AND s.transaction_merchant_id IS NOT NULL
          )
        )
      END
    ) AS merchant_applicable,
    (
      CASE
        WHEN v_directional_matching THEN (
          s.tx_is_in
          AND (
            (
              s.doc_customer_ns IS NOT NULL
              AND s.tx_customer_ns IS NOT NULL
            )
            OR (
              (
                s.doc_iban_ns IS NOT NULL
                AND s.tx_iban_beneficiary_ns IS NOT NULL
                AND s.doc_iban_ns = s.tx_iban_beneficiary_ns
              )
              OR (
                s.doc_iban_core_ns IS NOT NULL
                AND s.tx_iban_beneficiary_core_ns IS NOT NULL
                AND s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns
              )
            )
          )
        )
        ELSE (
          (
            s.doc_customer_ns IS NOT NULL
            AND s.tx_customer_ns IS NOT NULL
          )
          OR (
            (
              s.doc_iban_ns IS NOT NULL
              AND s.tx_iban_beneficiary_ns IS NOT NULL
              AND s.doc_iban_ns = s.tx_iban_beneficiary_ns
            )
            OR (
              s.doc_iban_core_ns IS NOT NULL
              AND s.tx_iban_beneficiary_core_ns IS NOT NULL
              AND s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns
            )
          )
        )
      END
    ) AS customer_applicable
  FROM scored_components s
),
scored_with_confidence AS (
  SELECT s.*,
    (
      s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight + (
        CASE
          WHEN s.merchant_applicable THEN s.merchant_weight
          ELSE 0
        END
      ) + (
        CASE
          WHEN s.customer_applicable THEN s.customer_weight
          ELSE 0
        END
      )
    )::numeric AS dynamic_total_weight,
    (
      s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + (
        CASE
          WHEN s.merchant_applicable THEN s.merchant_score
          ELSE 0
        END
      ) + (
        CASE
          WHEN s.customer_applicable THEN s.customer_score
          ELSE 0
        END
      )
    )::numeric AS total_numerator,
    (
      COALESCE(s.amount_match::int, 0) + COALESCE(s.invoice_number_match::int, 0) + COALESCE(s.order_number_match::int, 0) + COALESCE(s.date_match::int, 0) + COALESCE(s.currency_match::int, 0) + COALESCE(s.merchant_match::int, 0) + COALESCE(s.customer_match::int, 0)
    ) AS signals_matched_count,
    CASE
      WHEN (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight + (
          CASE
            WHEN s.merchant_applicable THEN s.merchant_weight
            ELSE 0
          END
        ) + (
          CASE
            WHEN s.customer_applicable THEN s.customer_weight
            ELSE 0
          END
        )
      ) > 0 THEN ROUND(
        (
          (
            s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + (
              CASE
                WHEN s.merchant_applicable THEN s.merchant_score
                ELSE 0
              END
            ) + (
              CASE
                WHEN s.customer_applicable THEN s.customer_score
                ELSE 0
              END
            )
          ) / (
            s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight + (
              CASE
                WHEN s.merchant_applicable THEN s.merchant_weight
                ELSE 0
              END
            ) + (
              CASE
                WHEN s.customer_applicable THEN s.customer_weight
                ELSE 0
              END
            )
          )
        ) * 100.0,
        4
      )
      ELSE 0
    END AS confidence_percent
  FROM scored_with_applicability s
),
low_signal AS (
  SELECT *
  FROM scored_with_confidence
  WHERE signals_matched_count < 1
),
below_min_confidence AS (
  SELECT *
  FROM scored_with_confidence
  WHERE signals_matched_count >= 1
    AND confidence_percent < v_config_min_confidence
),
base_candidates AS (
  SELECT *
  FROM scored_with_confidence
  WHERE signals_matched_count >= 1
    AND confidence_percent >= v_config_min_confidence
),
ranked_per_transaction AS (
  SELECT b.*,
    ROW_NUMBER() OVER (
      PARTITION BY b.transaction_id
      ORDER BY b.confidence_percent DESC,
        b.total_numerator DESC,
        b.signals_matched_count DESC,
        b.document_id
    ) AS rownum_per_transaction
  FROM base_candidates b
),
not_top_for_transaction AS (
  SELECT *
  FROM ranked_per_transaction
  WHERE rownum_per_transaction > 1
),
top_per_transaction AS (
  SELECT *
  FROM ranked_per_transaction
  WHERE rownum_per_transaction = 1
),
ranked_per_document AS (
  SELECT t.*,
    ROW_NUMBER() OVER (
      PARTITION BY t.doc_dedup_key
      ORDER BY t.confidence_percent DESC,
        t.total_numerator DESC,
        t.signals_matched_count DESC,
        ABS(t.transaction_date_unix - t.document_date_unix) ASC,
        t.document_id
    ) AS rownum_per_document
  FROM top_per_transaction t
),
not_top_for_document AS (
  SELECT *
  FROM ranked_per_document
  WHERE rownum_per_document > 1
),
top_per_document AS (
  SELECT *
  FROM ranked_per_document
  WHERE rownum_per_document = 1
),
final_candidates AS (
  SELECT *
  FROM top_per_document
  ORDER BY confidence_percent DESC,
    total_numerator DESC,
    signals_matched_count DESC,
    transaction_id,
    document_id
),
allowed_for_insert AS (
  SELECT f.*
  FROM final_candidates f
  WHERE NOT EXISTS (
      SELECT 1
      FROM public.reconciliations r1
      WHERE r1.transaction_id = f.transaction_id
    )
    AND NOT EXISTS (
      SELECT 1
      FROM public.reconciliations r2
      WHERE r2.document_id = f.document_id
    )
),
inserted_reconciliations AS (
  INSERT INTO public.reconciliations (
      transaction_id,
      document_id,
      company_id,
      confidence,
      time,
      engine_version,
      fiscal_year,
      quarter,
      month,
      summary
    ) WITH ts_base AS (
      SELECT f.transaction_id,
        f.document_id,
        f.document_company_id,
        f.confidence_percent,
        COALESCE(
          f.transaction_date_unix,
          f.document_date_unix,
          v_log_run_unix_time
        )::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT d.transaction_id,
        d.document_id,
        d.document_company_id,
        d.confidence_percent,
        d.invoice_number_match AS invoice_match,
        d.order_number_match AS order_match,
        d.amount_match,
        d.date_match,
        d.currency_match,
        d.merchant_match,
        d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        t.base_unix,
        a.tx_invoice_norm,
        a.doc_invoice_norm,
        a.tx_order_norm,
        a.doc_order_norm,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name,
        a.doc_invoice_raw,
        a.doc_order_raw,
        a.tx_iban_beneficiary_ns,
        a.tx_iban_counterparty_ns,
        a.doc_iban_ns,
        a.tx_iban_beneficiary_core_ns,
        a.tx_iban_counterparty_core_ns,
        a.doc_iban_core_ns,
        a.tx_is_in,
        a.tx_is_out
      FROM ts_base t
        JOIN final_candidates d ON d.transaction_id = t.transaction_id
        AND d.document_id = t.document_id
        JOIN attributes a ON a.transaction_id = d.transaction_id
        AND a.document_id = d.document_id
    )
  SELECT e.transaction_id,
    e.document_id,
    e.document_company_id,
    COALESCE(e.confidence_percent, 0)::numeric,
    v_log_run_unix_time,
    v_engine_version,
    EXTRACT(
      YEAR
      FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC')
    )::int,
    EXTRACT(
      QUARTER
      FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC')
    )::int,
    to_char(
      (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'),
      'FMMonth'
    ),
    json_build_object(
      'signals',
      (
        SELECT json_agg(s)
        FROM unnest(
            ARRAY [
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]
          ) s
        WHERE s IS NOT NULL
      ),
      'tx_amount',
      e.transaction_effective_amount,
      'doc_total',
      e.document_total,
      'tx_invoice',
      e.tx_invoice_norm,
      'doc_invoice',
      COALESCE(e.doc_invoice_raw, e.doc_invoice_norm),
      'tx_order',
      e.tx_order_norm,
      'doc_order',
      COALESCE(e.doc_order_raw, e.doc_order_norm),
      'tx_currency',
      e.transaction_currency,
      'doc_currency',
      e.document_currency,
      'tx_merchant',
      e.transaction_merchant_id,
      'doc_merchant',
      e.document_merchant_id,
      'tx_customer',
      e.transaction_customer_name,
      'doc_customer',
      e.document_customer_name,
      'tx_unix',
      e.transaction_date_unix,
      'doc_unix',
      e.document_date_unix,
      'tx_iban_beneficiary',
      e.tx_iban_beneficiary_ns,
      'tx_iban_counterparty',
      e.tx_iban_counterparty_ns,
      'doc_iban',
      e.doc_iban_ns,
      'tx_iban_beneficiary_core',
      e.tx_iban_beneficiary_core_ns,
      'tx_iban_counterparty_core',
      e.tx_iban_counterparty_core_ns,
      'doc_iban_core',
      e.doc_iban_core_ns,
      'tx_is_in',
      e.tx_is_in,
      'tx_is_out',
      e.tx_is_out,
      'direction',
      CASE
        WHEN e.tx_is_in THEN 'incoming'
        WHEN e.tx_is_out THEN 'outgoing'
        ELSE 'unknown'
      END,
      'days_between_tx_and_doc',
      CASE
        WHEN e.transaction_date_unix IS NOT NULL
        AND e.document_date_unix IS NOT NULL THEN (
          ABS(e.transaction_date_unix - e.document_date_unix) / 86400
        )::int
        ELSE NULL
      END
    )::text
  FROM enrich e
  RETURNING reconciliation_id,
    transaction_id,
    document_id,
    confidence
),
inserted_match_scores AS (
  INSERT INTO public.reconciliations_match_scores (
      reconciliation_id,
      company_id,
      confidence,
      numerator,
      total_weight,
      date_match,
      invoice_match,
      order_match,
      merchant_match,
      amount_match,
      currency_match,
      customer_match,
      date_weight,
      invoice_weight,
      order_weight,
      merchant_weight,
      amount_weight,
      currency_weight,
      customer_weight,
      date_score,
      invoice_score,
      order_score,
      merchant_score,
      amount_score,
      currency_score,
      customer_score
    )
  SELECT r.reconciliation_id,
    d.document_company_id AS company_id,
    r.confidence,
    d.total_numerator,
    d.dynamic_total_weight,
    d.date_match,
    d.invoice_number_match,
    d.order_number_match,
    d.merchant_match,
    (d.amount_match)::bool,
    d.currency_match,
    d.customer_match,
    d.date_weight,
    d.invoice_weight,
    d.order_weight,
    d.merchant_weight,
    d.amount_weight,
    d.currency_weight,
    d.customer_weight,
    d.date_score,
    d.invoice_score,
    d.order_score,
    d.merchant_score,
    d.amount_score,
    d.currency_score,
    d.customer_score
  FROM inserted_reconciliations r
    JOIN scored_with_confidence d ON d.transaction_id = r.transaction_id
    AND d.document_id = r.document_id
  RETURNING 1
),
final_counts AS (
  SELECT (
      SELECT COUNT(*)
      FROM top_per_transaction
    ) AS top_per_transaction_count,
    (
      SELECT COUNT(*)
      FROM top_per_document
    ) AS top_per_document_count,
    (
      SELECT COUNT(*)
      FROM final_candidates
    ) AS passed_confidence_count,
    (
      SELECT COUNT(*)
      FROM inserted_reconciliations
    ) AS inserted_count,
    (
      SELECT COUNT(*)
      FROM base_candidates
    ) AS candidates_count,
    (
      SELECT COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb)
      FROM seed_counts_total
    ) AS seed_counts_total_json,
    (
      SELECT COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb)
      FROM seed_counts_distinct
    ) AS seed_counts_distinct_json,
    (
      SELECT COUNT(*)
      FROM transactions_scope
    ) AS total_tx_count,
    (
      SELECT COUNT(*)
      FROM document_scope
    ) AS total_doc_count
)
SELECT top_per_transaction_count,
  top_per_document_count,
  passed_confidence_count,
  inserted_count,
  candidates_count,
  seed_counts_total_json,
  seed_counts_distinct_json,
  total_tx_count,
  total_doc_count INTO v_summary_top_per_tx,
  v_summary_top_per_doc,
  v_summary_passed_count,
  v_inserted_count,
  v_candidates_count,
  v_seed_counts_total,
  v_seed_counts_distinct,
  v_total_tx,
  v_total_docs
FROM final_counts;
-- end log
v_elapsed_ms := FLOOR(
  EXTRACT(
    EPOCH
    FROM (clock_timestamp() - v_start_time)
  ) * 1000
)::bigint;
INSERT INTO public.reconciliations_logs (
    run_id,
    function_version,
    action,
    target,
    status,
    message,
    company_id,
    transaction_id,
    document_id,
    reconciliation_id,
    rows_affected,
    confidence,
    step_ms,
    processing_ms,
    context
  )
VALUES (
    v_log_run_id,
    v_engine_version,
    'run_end',
    'function',
    'ok',
    NULL,
    p_company_id,
    NULL,
    NULL,
    NULL,
    v_inserted_count,
    NULL,
    NULL,
    v_elapsed_ms,
    jsonb_build_object(
      'inserted',
      v_inserted_count,
      'candidates',
      v_candidates_count,
      'transactions_scanned',
      v_total_tx,
      'documents_considered',
      v_total_docs,
      'top_per_tx',
      v_summary_top_per_tx,
      'top_per_doc',
      v_summary_top_per_doc,
      'passed_confidence',
      v_summary_passed_count,
      'seed_candidates_by_logic',
      v_seed_counts_total,
      'seed_unique_pairs_by_logic',
      v_seed_counts_distinct
    )
  );
RETURN jsonb_build_object(
  'msg',
  v_status_message,
  'time_ms',
  v_elapsed_ms,
  'transactions_scanned',
  v_total_tx,
  'documents_considered',
  v_total_docs,
  'top_per_tx',
  v_summary_top_per_tx,
  'top_per_doc',
  v_summary_top_per_doc,
  'passed_confidence',
  v_summary_passed_count,
  'seed_candidates_by_logic',
  v_seed_counts_total,
  'seed_unique_pairs_by_logic',
  v_seed_counts_distinct
);
EXCEPTION
WHEN OTHERS THEN v_elapsed_ms := FLOOR(
  EXTRACT(
    EPOCH
    FROM (clock_timestamp() - v_start_time)
  ) * 1000
)::bigint;
GET STACKED DIAGNOSTICS v_err_state = RETURNED_SQLSTATE,
v_err_msg = MESSAGE_TEXT,
v_err_detail = PG_EXCEPTION_DETAIL,
v_err_hint = PG_EXCEPTION_HINT,
v_err_context = PG_EXCEPTION_CONTEXT;
INSERT INTO public.reconciliations_logs (
    run_id,
    function_version,
    action,
    target,
    status,
    message,
    company_id,
    transaction_id,
    document_id,
    reconciliation_id,
    rows_affected,
    confidence,
    step_ms,
    processing_ms,
    context
  )
VALUES (
    v_log_run_id,
    v_engine_version,
    'run_error',
    'function',
    'error',
    v_err_msg,
    p_company_id,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    v_elapsed_ms,
    jsonb_build_object(
      'sqlstate',
      v_err_state,
      'detail',
      v_err_detail,
      'hint',
      v_err_hint,
      'context',
      v_err_context
    )
  );
RETURN jsonb_build_object(
  'msg',
  'error',
  'time_ms',
  v_elapsed_ms,
  'transactions_scanned',
  0,
  'documents_considered',
  0,
  'top_per_tx',
  0,
  'top_per_doc',
  0,
  'passed_confidence',
  0,
  'error',
  v_err_msg,
  'sqlstate',
  v_err_state
);
END;
$$;

alter function athena_reconcile_experimental(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_experimental(unknown, unknown) to anon;

grant execute on function athena_reconcile_experimental(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_experimental(unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v1_9_n(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_status_message text := 'success';
  v_engine_version text := '1.9M';

  v_start_time timestamptz := clock_timestamp();
  v_elapsed_ms bigint := 0;

  -- config
  v_config_amount_tolerance           numeric;
  v_config_date_window_days           numeric;
  v_config_min_confidence             numeric;
  v_config_allow_document_duplicates  boolean;
  v_config_log_activity               boolean;

  -- derived
  v_amount_tolerance_fraction numeric;
  v_date_window_seconds       bigint;
  v_date_full_weight_grace_days int := 30;

  -- Gaussian params
  v_edge_weight_small constant numeric := 0.10; -- score at the boundary (10%)
  v_gauss_k            constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small)); -- ~2.146

  v_cap_precise               int := 5;
  v_cap_broad                 int := 10;
  v_cap_window_only           int := 3;
  v_cap_iban_exact            int := 5;
  v_iban_exact_window_seconds bigint;
  v_max_transactions_to_scan  int := 20000;

  -- weights
  v_weights_json           jsonb := '{}'::jsonb;
  v_weights_total_possible numeric := 0;

  -- counts
  v_summary_top_per_tx   bigint := 0;
  v_summary_top_per_doc  bigint := 0;
  v_summary_passed_count bigint := 0;
  v_inserted_count       bigint := 0;
  v_candidates_count     bigint := 0;

  -- input sizes
  v_total_tx   bigint := 0;
  v_total_docs bigint := 0;

  -- per-seed stats
  v_seed_counts_total    jsonb := '{}'::jsonb;
  v_seed_counts_distinct jsonb := '{}'::jsonb;

  -- run meta
  v_log_run_id        uuid := gen_random_uuid();
  v_log_run_unix_time bigint := EXTRACT(EPOCH FROM now())::bigint;

  -- matching mode
  v_directional_matching boolean := TRUE;

  -- errors
  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;
BEGIN
  -- config
  SELECT amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  INTO   v_config_amount_tolerance, v_config_date_window_days, v_config_min_confidence,
         v_config_allow_document_duplicates, v_config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    v_elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - v_start_time)) * 1000)::bigint;
    RETURN jsonb_build_object('msg','reconciliations_config not found','time_ms',v_elapsed_ms,
                              'transactions_scanned',0,'documents_considered',0,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  -- tolerances/windows
  v_amount_tolerance_fraction := CASE
    WHEN v_config_amount_tolerance >= 1 THEN v_config_amount_tolerance / 100.0
    ELSE v_config_amount_tolerance
  END;

  v_date_window_seconds := CEIL(COALESCE(v_config_date_window_days, 1) * 86400)::bigint;
  v_iban_exact_window_seconds := LEAST(v_date_window_seconds, 14 * 86400)::bigint;

  IF v_config_min_confidence IS NULL THEN v_config_min_confidence := 0; END IF;
  IF v_config_min_confidence <= 1 THEN v_config_min_confidence := v_config_min_confidence * 100.0; END IF;

  -- weights
  WITH candidate_weights AS (
    SELECT LOWER(TRIM(w.weight_key)) AS k_norm,
           (w.weight_value)::numeric AS v_norm,
           w.company_id
    FROM public.reconciliations_weight_registry w
    WHERE w.active
      AND w.merchant_id IS NULL
      AND (
           (p_company_id IS NULL AND w.company_id IS NULL)
        OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR w.company_id IS NULL))
      )
      AND LOWER(TRIM(w.weight_key)) IN (
        'amount_total','invoice_number','order_number','date','currency','merchant_id','customer_name'
      )
  ),
  picked AS (
    SELECT DISTINCT ON (k_norm) k_norm, v_norm
    FROM candidate_weights
    ORDER BY k_norm, (company_id IS NULL)
  )
  SELECT COALESCE(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) INTO v_weights_json FROM picked;

  SELECT COALESCE((v_weights_json->>'amount_total')::numeric,0)
       + COALESCE((v_weights_json->>'invoice_number')::numeric,0)
       + COALESCE((v_weights_json->>'order_number')::numeric,0)
       + COALESCE((v_weights_json->>'date')::numeric,0)
       + COALESCE((v_weights_json->>'currency')::numeric,0)
       + COALESCE((v_weights_json->>'merchant_id')::numeric,0)
       + COALESCE((v_weights_json->>'customer_name')::numeric,0)
  INTO v_weights_total_possible;

  IF v_weights_total_possible = 0 THEN
    v_weights_json := jsonb_build_object(
      'amount_total',1,'invoice_number',1,'order_number',1,'date',1,'currency',1,'merchant_id',1,'customer_name',1
    );
    v_weights_total_possible := 7;
  END IF;

  -- run_start log
  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    v_log_run_id, v_engine_version, 'run_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', v_config_log_activity,
        'allow_doc_dups', v_config_allow_document_duplicates,
        'amount_tol_pct', v_amount_tolerance_fraction,
        'min_confidence', v_config_min_confidence,
        'date_window_days', v_config_date_window_days,
        'max_tx_seed', v_max_transactions_to_scan,
        'cap_precise', v_cap_precise,
        'cap_broad',   v_cap_broad,
        'cap_window_only', v_cap_window_only,
        'cap_iban_exact', v_cap_iban_exact,
        'iban_exact_window_days', ROUND(v_iban_exact_window_seconds/86400.0,2),
        'directional_matching', v_directional_matching,
        'gauss_edge_weight', v_edge_weight_small
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  -- ====================== pipeline ======================
  WITH
  document_scope AS (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number_raw,
      NULLIF(d.order_id, '')       AS order_id_raw,
      NULLIF(public.norm_code(d.invoice_number), '') AS invoice_number_norm,
      NULLIF(public.norm_code(d.order_id), '')       AS order_id_norm,
      CASE
        WHEN d.document_date_unix IS NOT NULL THEN d.document_date_unix
        WHEN d.document_date ~ '^\d{4}-[A-Za-z]+-\d{1,2}$' THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'YYYY-Month-FMDD'))::bigint
        WHEN d.document_date ~ '^\d{1,2}-[A-Za-z]+-\d{4}$' THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'FMDD-Month-YYYY'))::bigint
        WHEN d.document_date ~ '^\d{4}-\d{1,2}-\d{1,2}$'   THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'YYYY-FMMM-FMDD'))::bigint
        WHEN d.document_date ~ '^\d{1,2}-\d{1,2}-\d{4}$'   THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'FMDD-FMMM-YYYY'))::bigint
        ELSE NULL
      END AS document_date_unix,
      d.merchant_id,
      d.customer_name,
      NULLIF(public.norm_core_ns(COALESCE(d.merchant_name, d.merchant_info::jsonb->>'name')), '') AS doc_merchant_ns,
      NULLIF(public.norm_text_ns(
        concat_ws(' ',
          d.customer_name,
          d.customer_info::jsonb->>'name',
          d.customer_info::jsonb->>'contact_person',
          d.customer_info::jsonb->>'company_name',
          d.customer_info::jsonb->>'email'
        )
      ), '') AS doc_customer_ns,
      NULLIF(public.norm_iban(d.merchant_info::jsonb->>'iban'), '')      AS doc_iban_ns,
      NULLIF(public.norm_iban_core(d.merchant_info::jsonb->>'iban'), '') AS doc_iban_core_ns,
      COALESCE(
        d.customer_name,
        d.customer_info::jsonb->>'name',
        d.customer_info::jsonb->>'contact_person',
        d.customer_info::jsonb->>'company_name',
        d.customer_info::jsonb->>'email'
      ) AS doc_customer_display,
      COALESCE(NULLIF(d.dedup_serialized,''), d.document_id::text) AS doc_dedup_key
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),

  transactions_scope AS (
    SELECT
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,

      NULLIF(
        public.norm_code(
          COALESCE(
            NULLIF(t.invoice_nr,''),
            (SELECT m[1]
             FROM regexp_matches(
               UPPER(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
               '(INV(?:OICE)?[ -_/]?\d{1,20}|[A-Z]{1,6}[-/]\d{2,20}|[0-9]{4}[-/][0-9]{2,20})',
               'g'
             ) AS m
             LIMIT 1)
          )
        ),
      '') AS invoice_nr_norm,

      NULLIF(
        public.norm_code(
          COALESCE(
            NULLIF(t.order_id,''),
            (SELECT m[1]
             FROM regexp_matches(
               UPPER(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
               '(ORD(?:ER)?[ -_/]?\d{2,20}|PO[ -_/]?\d{2,20}|ORDER[ -_/]?\d{2,20})',
               'g'
             ) AS m
             LIMIT 1)
          )
        ),
      '') AS order_id_norm,

      t.completed_date,
      t.merchant_id,
      t.name_consumer,

      (NULLIF(t.credited,0) IS NOT NULL OR NULLIF(t.money_in,0) IS NOT NULL)  AS tx_is_in,
      (NULLIF(t.debited,0)  IS NOT NULL OR NULLIF(t.money_out,0) IS NOT NULL) AS tx_is_out,

      NULLIF(public.norm_core_ns(
        COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
      ), '') AS tx_merchant_ns,

      NULLIF(public.norm_text_ns(
        COALESCE(
          t.name_consumer,
          t.account_holder_name,
          t.account_holder_name_counterparty
        )
      ), '') AS tx_customer_ns,

      NULLIF(public.norm_iban(COALESCE(t.iban_beneficiary, t.iban, '')), '')      AS tx_iban_beneficiary_ns,
      NULLIF(public.norm_iban(COALESCE(t.iban_counterparty, '')), '')             AS tx_iban_counterparty_ns,
      NULLIF(public.norm_iban_core(COALESCE(t.iban_beneficiary, t.iban, '')), '') AS tx_iban_beneficiary_core_ns,
      NULLIF(public.norm_iban_core(COALESCE(t.iban_counterparty, '')), '')        AS tx_iban_counterparty_core_ns,

      COALESCE(
        t.name_consumer,
        t.account_holder_name,
        t.account_holder_name_counterparty
      ) AS tx_customer_display,

      CASE
        WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN t.debited
        WHEN t.credited IS NOT NULL AND t.credited > 0 THEN t.credited
        WHEN t.amount   IS NOT NULL                  THEN ABS(t.amount)
        ELSE NULL
      END AS effective_amount
    FROM public.transactions t
    WHERE (p_company_id IS NULL OR t.company_id = p_company_id::text)
      AND t.reconciled IS NOT TRUE
      AND (
           t.invoice_nr IS NOT NULL
        OR t.order_id   IS NOT NULL
        OR (t.currency IS NOT NULL AND t.merchant_id   IS NOT NULL)
        OR (t.currency IS NOT NULL AND (t.name_consumer IS NOT NULL OR t.account_holder_name IS NOT NULL OR t.account_holder_name_counterparty IS NOT NULL))
        OR (t.currency IS NOT NULL AND (t.amount IS NOT NULL OR t.debited IS NOT NULL OR t.credited IS NOT NULL))
      )
    ORDER BY t.completed_date DESC, t.transaction_id
    LIMIT v_max_transactions_to_scan
  ),

  /* ================== staged seeding (specific → broad) ================== */

  s1_invoice AS (
    SELECT t.transaction_id, d.document_id, 'invoice_number' AS seed
    FROM transactions_scope t
    JOIN document_scope d
      ON d.company_id = t.company_id::uuid
     AND d.invoice_number_norm IS NOT NULL
     AND t.invoice_nr_norm     IS NOT NULL
     AND d.invoice_number_norm = t.invoice_nr_norm
     AND d.currency = t.currency
  ),

  s2_order AS (
    SELECT s.*
    FROM (
      SELECT t.transaction_id, d.document_id, 'order_number' AS seed
      FROM transactions_scope t
      JOIN document_scope d
        ON d.company_id = t.company_id::uuid
       AND d.order_id_norm IS NOT NULL
       AND t.order_id_norm IS NOT NULL
       AND d.order_id_norm = t.order_id_norm
       AND d.currency = t.currency
    ) s
    LEFT JOIN (SELECT DISTINCT transaction_id FROM s1_invoice) u USING (transaction_id)
    WHERE u.transaction_id IS NULL
  ),

  -- IBAN exact amount (no date window)
  s3_iban_exact_amount_out AS (
    SELECT t.transaction_id, d_sub.document_id, 'iban_exact_amount_out' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND ((NOT v_directional_matching) OR t.tx_is_out)
        AND d.currency = t.currency
        AND t.debited IS NOT NULL AND t.debited > 0
        AND d.total = t.debited
        AND (
             (d.doc_iban_ns IS NOT NULL AND t.tx_iban_counterparty_ns IS NOT NULL AND d.doc_iban_ns = t.tx_iban_counterparty_ns)
          OR (d.doc_iban_core_ns IS NOT NULL AND t.tx_iban_counterparty_core_ns IS NOT NULL AND d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns)
        )
      ORDER BY d.document_id
      LIMIT v_cap_iban_exact
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),
  s3_iban_exact_amount_in AS (
    SELECT t.transaction_id, d_sub.document_id, 'iban_exact_amount_in' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND ((NOT v_directional_matching) OR t.tx_is_in)
        AND d.currency = t.currency
        AND t.credited IS NOT NULL AND t.credited > 0
        AND d.total = t.credited
        AND (
             (d.doc_iban_ns IS NOT NULL AND t.tx_iban_beneficiary_ns IS NOT NULL AND d.doc_iban_ns = t.tx_iban_beneficiary_ns)
          OR (d.doc_iban_core_ns IS NOT NULL AND t.tx_iban_beneficiary_core_ns IS NOT NULL AND d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns)
        )
      ORDER BY d.document_id
      LIMIT v_cap_iban_exact
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),
  s3_iban_exact_amount AS (
    SELECT * FROM s3_iban_exact_amount_out
    UNION ALL
    SELECT * FROM s3_iban_exact_amount_in
  ),

  s4_merchant_id_exact_amount AS (
    SELECT s.*
    FROM (
      SELECT t.transaction_id, d.document_id, 'merchant_id_exact_amount' AS seed
      FROM transactions_scope t
      JOIN document_scope d
        ON d.company_id = t.company_id::uuid
       AND ((NOT v_directional_matching) OR t.tx_is_out)
       AND d.currency = t.currency
       AND d.merchant_id = t.merchant_id
       AND t.debited IS NOT NULL AND t.debited > 0
       AND d.total = t.debited
       AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
       AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
    ) s
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
    ) done USING (transaction_id)
    WHERE done.transaction_id IS NULL
  ),

  s5_merchant_ns_exact_amount AS (
    SELECT s.*
    FROM (
      SELECT t.transaction_id, d.document_id, 'merchant_ns_exact_amount' AS seed
      FROM transactions_scope t
      JOIN document_scope d
        ON d.company_id = t.company_id::uuid
       AND ((NOT v_directional_matching) OR t.tx_is_out)
       AND d.currency = t.currency
       AND d.doc_merchant_ns IS NOT NULL AND t.tx_merchant_ns IS NOT NULL
       AND (
         d.doc_merchant_ns = t.tx_merchant_ns
         OR position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
         OR position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
       )
       AND t.debited IS NOT NULL AND t.debited > 0
       AND d.total = t.debited
       AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
       AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
    ) s
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
    ) done USING (transaction_id)
    WHERE done.transaction_id IS NULL
  ),

  s6_customer_ns_exact_amount AS (
    SELECT s.*
    FROM (
      SELECT t.transaction_id, d.document_id, 'customer_ns_exact_amount' AS seed
      FROM transactions_scope t
      JOIN document_scope d
        ON d.company_id = t.company_id::uuid
       AND ((NOT v_directional_matching) OR t.tx_is_in)
       AND d.currency = t.currency
       AND d.doc_customer_ns IS NOT NULL AND t.tx_customer_ns IS NOT NULL
       AND (
         d.doc_customer_ns = t.tx_customer_ns
         OR position(d.doc_customer_ns in t.tx_customer_ns) > 0
         OR position(t.tx_customer_ns in d.doc_customer_ns) > 0
       )
       AND t.credited IS NOT NULL AND t.credited > 0
       AND d.total = t.credited
       AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
       AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
    ) s
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s5_merchant_ns_exact_amount
    ) done USING (transaction_id)
    WHERE done.transaction_id IS NULL
  ),

  s7_iban_plus_signal AS (
    SELECT t.transaction_id, d_sub.document_id, 'iban_plus_signal' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s5_merchant_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s6_customer_ns_exact_amount
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
        AND (
             ( ((NOT v_directional_matching) OR t.tx_is_out) AND
               ( (d.doc_iban_ns IS NOT NULL AND t.tx_iban_counterparty_ns IS NOT NULL AND d.doc_iban_ns = t.tx_iban_counterparty_ns)
                 OR (d.doc_iban_core_ns IS NOT NULL AND t.tx_iban_counterparty_core_ns IS NOT NULL AND d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns) ) )
          OR ( ((NOT v_directional_matching) OR t.tx_is_in ) AND
               ( (d.doc_iban_ns IS NOT NULL AND t.tx_iban_beneficiary_ns IS NOT NULL AND d.doc_iban_ns = t.tx_iban_beneficiary_ns)
                 OR (d.doc_iban_core_ns IS NOT NULL AND t.tx_iban_beneficiary_core_ns IS NOT NULL AND d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns) ) )
        )
        AND (
             (t.invoice_nr_norm IS NOT NULL AND d.invoice_number_norm IS NOT NULL)
          OR (t.order_id_norm   IS NOT NULL AND d.order_id_norm   IS NOT NULL)
          OR (t.tx_merchant_ns  IS NOT NULL AND d.doc_merchant_ns IS NOT NULL)
          OR (t.tx_customer_ns  IS NOT NULL AND d.doc_customer_ns IS NOT NULL)
        )
      ORDER BY d.document_date_unix, d.document_id
      LIMIT v_cap_precise
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),

  s8_merchant_id_tol AS (
    SELECT t.transaction_id, d_sub.document_id, 'merchant_id_tol' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s5_merchant_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s6_customer_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s7_iban_plus_signal
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND ((NOT v_directional_matching) OR t.tx_is_out)
        AND d.currency = t.currency
        AND d.merchant_id = t.merchant_id
        AND v_amount_tolerance_fraction > 0
        AND t.debited IS NOT NULL AND t.debited > 0
        AND d.total BETWEEN (t.debited / (1 + v_amount_tolerance_fraction))
                        AND (t.debited / (1 - v_amount_tolerance_fraction))
        AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
      ORDER BY d.document_date_unix, d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),

  s9_merchant_ns_tol AS (
    SELECT t.transaction_id, d_sub.document_id, 'merchant_ns_tol' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s5_merchant_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s6_customer_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s7_iban_plus_signal
      UNION ALL SELECT DISTINCT transaction_id FROM s8_merchant_id_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND ((NOT v_directional_matching) OR t.tx_is_out)
        AND d.currency = t.currency
        AND d.doc_merchant_ns IS NOT NULL AND t.tx_merchant_ns IS NOT NULL
        AND (
          d.doc_merchant_ns = t.tx_merchant_ns
          OR position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
          OR position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
        )
        AND v_amount_tolerance_fraction > 0
        AND t.debited IS NOT NULL AND t.debited > 0
        AND d.total BETWEEN (t.debited / (1 + v_amount_tolerance_fraction))
                        AND (t.debited / (1 - v_amount_tolerance_fraction))
        AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
      ORDER BY d.document_date_unix, d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),

  s10_customer_ns_tol AS (
    SELECT t.transaction_id, d_sub.document_id, 'customer_ns_tol' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s5_merchant_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s6_customer_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s7_iban_plus_signal
      UNION ALL SELECT DISTINCT transaction_id FROM s8_merchant_id_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s9_merchant_ns_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND ((NOT v_directional_matching) OR t.tx_is_in)
        AND d.currency = t.currency
        AND d.doc_customer_ns IS NOT NULL AND t.tx_customer_ns IS NOT NULL
        AND (
          d.doc_customer_ns = t.tx_customer_ns
          OR position(d.doc_customer_ns in t.tx_customer_ns) > 0
          OR position(t.tx_customer_ns in d.doc_customer_ns) > 0
        )
        AND v_amount_tolerance_fraction > 0
        AND t.credited IS NOT NULL AND t.credited > 0
        AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                        AND (t.credited / (1 - v_amount_tolerance_fraction))
        AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
      ORDER BY d.document_date_unix, d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),

  s11_iban_tol AS (
    SELECT t.transaction_id, d_sub.document_id, 'iban_tol' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s5_merchant_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s6_customer_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s7_iban_plus_signal
      UNION ALL SELECT DISTINCT transaction_id FROM s8_merchant_id_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s9_merchant_ns_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s10_customer_ns_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
        AND v_amount_tolerance_fraction > 0
        AND (
          ( ((NOT v_directional_matching) OR t.tx_is_out) AND
            ( (d.doc_iban_ns IS NOT NULL AND t.tx_iban_counterparty_ns IS NOT NULL AND d.doc_iban_ns = t.tx_iban_counterparty_ns)
              OR (d.doc_iban_core_ns IS NOT NULL AND t.tx_iban_counterparty_core_ns IS NOT NULL AND d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns) )
            AND t.debited IS NOT NULL AND t.debited > 0
            AND d.total BETWEEN (t.debited  / (1 + v_amount_tolerance_fraction))
                            AND (t.debited  / (1 - v_amount_tolerance_fraction)) )
          OR
          ( ((NOT v_directional_matching) OR t.tx_is_in) AND
            ( (d.doc_iban_ns IS NOT NULL AND t.tx_iban_beneficiary_ns IS NOT NULL AND d.doc_iban_ns = t.tx_iban_beneficiary_ns)
              OR (d.doc_iban_core_ns IS NOT NULL AND t.tx_iban_beneficiary_core_ns IS NOT NULL AND d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns) )
            AND t.credited IS NOT NULL AND t.credited > 0
            AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                            AND (t.credited / (1 - v_amount_tolerance_fraction)) )
        )
      ORDER BY d.document_date_unix, d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),

  s12_window_amount_exact AS (
    SELECT t.transaction_id, d_sub.document_id, 'window_amount_exact' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s5_merchant_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s6_customer_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s7_iban_plus_signal
      UNION ALL SELECT DISTINCT transaction_id FROM s8_merchant_id_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s9_merchant_ns_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s10_customer_ns_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s11_iban_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
        AND (
          ( ((NOT v_directional_matching) OR t.tx_is_out) AND t.debited  IS NOT NULL AND t.debited  > 0 AND d.total = t.debited )
          OR
          ( ((NOT v_directional_matching) OR t.tx_is_in ) AND t.credited IS NOT NULL AND t.credited > 0 AND d.total = t.credited)
        )
      ORDER BY d.document_date_unix, d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),

  s13_window_amount_tol AS (
    SELECT t.transaction_id, d_sub.document_id, 'window_amount_tol' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s5_merchant_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s6_customer_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s7_iban_plus_signal
      UNION ALL SELECT DISTINCT transaction_id FROM s8_merchant_id_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s9_merchant_ns_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s10_customer_ns_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s11_iban_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s12_window_amount_exact
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
        AND v_amount_tolerance_fraction > 0
        AND (
          ( ((NOT v_directional_matching) OR t.tx_is_out) AND t.debited  IS NOT NULL AND t.debited  > 0
            AND d.total BETWEEN (t.debited  / (1 + v_amount_tolerance_fraction))
                            AND (t.debited  / (1 - v_amount_tolerance_fraction)) )
          OR
          ( ((NOT v_directional_matching) OR t.tx_is_in ) AND t.credited IS NOT NULL AND t.credited > 0
            AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                            AND (t.credited / (1 - v_amount_tolerance_fraction)) )
        )
      ORDER BY d.document_date_unix, d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),

  s14_window_only AS (
    SELECT t.transaction_id, d_sub.document_id, 'window_only' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s5_merchant_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s6_customer_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s7_iban_plus_signal
      UNION ALL SELECT DISTINCT transaction_id FROM s8_merchant_id_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s9_merchant_ns_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s10_customer_ns_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s11_iban_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s12_window_amount_exact
      UNION ALL SELECT DISTINCT transaction_id FROM s13_window_amount_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
      ORDER BY d.document_date_unix, d.document_id
      LIMIT v_cap_window_only
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),

  seeds AS (
    SELECT * FROM s1_invoice
    UNION ALL SELECT * FROM s2_order
    UNION ALL SELECT * FROM s3_iban_exact_amount
    UNION ALL SELECT * FROM s4_merchant_id_exact_amount
    UNION ALL SELECT * FROM s5_merchant_ns_exact_amount
    UNION ALL SELECT * FROM s6_customer_ns_exact_amount
    UNION ALL SELECT * FROM s7_iban_plus_signal
    UNION ALL SELECT * FROM s8_merchant_id_tol
    UNION ALL SELECT * FROM s9_merchant_ns_tol
    UNION ALL SELECT * FROM s10_customer_ns_tol
    UNION ALL SELECT * FROM s11_iban_tol
    UNION ALL SELECT * FROM s12_window_amount_exact
    UNION ALL SELECT * FROM s13_window_amount_tol
    UNION ALL SELECT * FROM s14_window_only
  ),

  seed_counts_total AS (
    SELECT seed, COUNT(*) AS ct FROM seeds GROUP BY seed
  ),
  seed_counts_distinct AS (
    SELECT seed, COUNT(DISTINCT (transaction_id, document_id)) AS ct FROM seeds GROUP BY seed
  ),

  candidate_pairs AS (
    SELECT DISTINCT transaction_id, document_id FROM seeds
  ),

  /* ---------------- scoring ---------------- */

  distinct_pairs AS (
    SELECT DISTINCT transaction_id, document_id
    FROM candidate_pairs
  ),

  attributes AS (
    SELECT
      p.transaction_id,
      p.document_id,
      t.company_id::uuid AS transaction_company_id,
      d.company_id       AS document_company_id,

      t.amount AS transaction_amount,
      t.debited AS transaction_debited,
      t.credited AS transaction_credited,
      d.total  AS document_total,
      t.currency AS transaction_currency,
      d.currency AS document_currency,

      t.invoice_nr_norm AS tx_invoice_norm,
      d.invoice_number_norm AS doc_invoice_norm,
      t.order_id_norm  AS tx_order_norm,
      d.order_id_norm  AS doc_order_norm,

      d.invoice_number_raw AS doc_invoice_raw,
      d.order_id_raw       AS doc_order_raw,

      t.completed_date AS transaction_date_unix,
      d.document_date_unix AS document_date_unix,
      t.merchant_id AS transaction_merchant_id,
      d.merchant_id AS document_merchant_id,

      t.tx_customer_display AS transaction_customer_name,
      d.doc_customer_display AS document_customer_name,

      t.tx_is_in, t.tx_is_out,
      t.tx_merchant_ns, d.doc_merchant_ns,
      t.tx_customer_ns, d.doc_customer_ns,

      t.tx_iban_beneficiary_ns,
      t.tx_iban_counterparty_ns,
      d.doc_iban_ns,
      t.tx_iban_beneficiary_core_ns,
      t.tx_iban_counterparty_core_ns,
      d.doc_iban_core_ns,

      COALESCE(NULLIF(d.doc_dedup_key,''), d.document_id::text) AS doc_dedup_key,

      t.effective_amount AS transaction_effective_amount
    FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope    d ON d.document_id    = p.document_id
    WHERE t.company_id::uuid = d.company_id
  ),

  evaluated AS (
    SELECT
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,

      COALESCE((a.tx_invoice_norm IS NOT NULL AND a.doc_invoice_norm IS NOT NULL
                AND a.tx_invoice_norm = a.doc_invoice_norm), FALSE) AS invoice_number_match,
      COALESCE((a.tx_order_norm   IS NOT NULL AND a.doc_order_norm   IS NOT NULL
                AND a.tx_order_norm = a.doc_order_norm), FALSE)     AS order_number_match,
      COALESCE(a.transaction_currency  = a.document_currency, FALSE) AS currency_match,

      (
        CASE
          WHEN v_directional_matching THEN
            ( a.tx_is_out AND (
                 (a.transaction_merchant_id IS NOT NULL AND a.document_merchant_id IS NOT NULL
                  AND a.transaction_merchant_id = a.document_merchant_id)
              OR (a.tx_merchant_ns IS NOT NULL AND a.doc_merchant_ns IS NOT NULL AND (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 OR position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 OR position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              OR (
                   (a.tx_iban_counterparty_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL AND a.tx_iban_counterparty_ns = a.doc_iban_ns)
                OR (a.tx_iban_counterparty_core_ns IS NOT NULL AND a.doc_iban_core_ns IS NOT NULL AND a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            ))
          ELSE
            (
                 (a.transaction_merchant_id IS NOT NULL AND a.document_merchant_id IS NOT NULL
                  AND a.transaction_merchant_id = a.document_merchant_id)
              OR (a.tx_merchant_ns IS NOT NULL AND a.doc_merchant_ns IS NOT NULL AND (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 OR position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 OR position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              OR (
                   (a.tx_iban_counterparty_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL AND a.tx_iban_counterparty_ns = a.doc_iban_ns)
                OR (a.tx_iban_counterparty_core_ns IS NOT NULL AND a.doc_iban_core_ns IS NOT NULL AND a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            )
        END
      ) AS merchant_match,

      (
        CASE
          WHEN v_directional_matching THEN
            ( a.tx_is_in AND (
                 (a.tx_customer_ns IS NOT NULL AND a.doc_customer_ns IS NOT NULL AND (
                    a.tx_customer_ns = a.doc_customer_ns
                 OR position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 OR position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              OR (
                   (a.tx_iban_beneficiary_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL AND a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                OR (a.tx_iban_beneficiary_core_ns IS NOT NULL AND a.doc_iban_core_ns IS NOT NULL AND a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            ))
          ELSE
            (
                 (a.tx_customer_ns IS NOT NULL AND a.doc_customer_ns IS NOT NULL AND (
                    a.tx_customer_ns = a.doc_customer_ns
                 OR position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 OR position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              OR (
                   (a.tx_iban_beneficiary_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL AND a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                OR (a.tx_iban_beneficiary_core_ns IS NOT NULL AND a.doc_iban_core_ns IS NOT NULL AND a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            )
        END
      ) AS customer_match,

      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, FALSE) AS date_within_cap,

      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND a.document_total = a.transaction_debited)  AS amount_exact_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND a.document_total = a.transaction_credited) AS amount_exact_from_credited,
      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND ABS(a.document_total - a.transaction_debited)  <= a.document_total * v_amount_tolerance_fraction) AS amount_tolerance_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND ABS(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) AS amount_tolerance_from_credited,

      a.tx_is_in, a.tx_is_out,

      -- carry for summary
      a.tx_iban_beneficiary_ns,
      a.tx_iban_counterparty_ns,
      a.doc_iban_ns,
      a.tx_iban_beneficiary_core_ns,
      a.tx_iban_counterparty_core_ns,
      a.doc_iban_core_ns,

      a.tx_merchant_ns,
      a.doc_merchant_ns,
      a.tx_customer_ns,
      a.doc_customer_ns,
      a.transaction_merchant_id,
      a.document_merchant_id
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.*,
      CASE
        WHEN e.tx_is_out AND e.amount_exact_from_debited THEN TRUE
        WHEN e.tx_is_in  AND e.amount_exact_from_credited THEN TRUE
        WHEN NOT v_directional_matching AND (e.amount_exact_from_debited OR e.amount_exact_from_credited) THEN TRUE
        ELSE FALSE
      END AS amount_exact_match,
      CASE
        WHEN v_amount_tolerance_fraction > 0 THEN
          CASE
            WHEN e.tx_is_out AND e.amount_tolerance_from_debited THEN TRUE
            WHEN e.tx_is_in  AND e.amount_tolerance_from_credited THEN TRUE
            WHEN NOT v_directional_matching AND (e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) THEN TRUE
            ELSE FALSE
          END
        ELSE FALSE
      END AS amount_tolerance_match,
      (e.amount_exact_from_debited OR e.amount_exact_from_credited OR e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) AS amount_direction_ok
    FROM evaluated e
  ),

  resolved_weights AS (
    SELECT
      COALESCE((v_weights_json->>'amount_total')::numeric, 0)   AS amount_weight,
      COALESCE((v_weights_json->>'invoice_number')::numeric, 0) AS invoice_weight,
      COALESCE((v_weights_json->>'order_number')::numeric, 0)   AS order_weight,
      COALESCE((v_weights_json->>'date')::numeric, 0)           AS date_weight,
      COALESCE((v_weights_json->>'currency')::numeric, 0)       AS currency_weight,
      COALESCE((v_weights_json->>'merchant_id')::numeric, 0)    AS merchant_weight,
      COALESCE((v_weights_json->>'customer_name')::numeric, 0)  AS customer_weight
  ),

  scored_components AS (
    SELECT
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,

      m.transaction_date_unix,
      m.document_date_unix,
      m.amount_direction_ok,

      m.tx_is_in, m.tx_is_out,

      -- carry fields for summary
      m.tx_iban_beneficiary_ns,
      m.tx_iban_counterparty_ns,
      m.doc_iban_ns,
      m.tx_iban_beneficiary_core_ns,
      m.tx_iban_counterparty_core_ns,
      m.doc_iban_core_ns,

      m.tx_merchant_ns,
      m.doc_merchant_ns,
      m.tx_customer_ns,
      m.doc_customer_ns,
      m.transaction_merchant_id,
      m.document_merchant_id,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN
            w.amount_weight * EXP(
              -0.5 * POWER(
                (ABS(m.transaction_effective_amount - m.document_total))
                / NULLIF((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0)
              , 2)
            )
          ELSE 0
        END, 0
      )::numeric AS amount_score,

      (m.amount_exact_match OR m.amount_tolerance_match) AS amount_match,
      m.invoice_number_match  AS invoice_number_match,
      m.order_number_match    AS order_number_match,

      CASE
        WHEN m.transaction_date_unix IS NOT NULL AND m.document_date_unix IS NOT NULL THEN
          w.date_weight * EXP(
            -0.5 * POWER(
              GREATEST(0,
                (ABS(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days
              ) / NULLIF( GREATEST(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k ), 0)
            , 2)
          )
        ELSE 0
      END::numeric AS date_score,

      m.date_within_cap AS date_match,

      m.currency_match  AS currency_match,
      m.merchant_match  AS merchant_match,
      m.customer_match  AS customer_match,

      w.amount_weight   AS amount_weight,
      w.invoice_weight  AS invoice_weight,
      w.order_weight    AS order_weight,
      w.date_weight     AS date_weight,
      w.currency_weight AS currency_weight,
      w.merchant_weight AS merchant_weight,
      w.customer_weight AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0)  * w.invoice_weight)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)    * w.order_weight)::numeric    AS order_score,
      (COALESCE(m.currency_match::int,0)        * w.currency_weight)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)        * w.merchant_weight)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)        * w.customer_weight)::numeric AS customer_score,

      m.transaction_effective_amount,
      m.document_total
    FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
  ),

  scored_with_applicability AS (
    SELECT
      s.*,

      (
        CASE
          WHEN v_directional_matching THEN
            ( s.tx_is_out AND (
                 (s.doc_merchant_ns IS NOT NULL AND s.tx_merchant_ns IS NOT NULL)
              OR ( (s.doc_iban_ns IS NOT NULL AND s.tx_iban_counterparty_ns IS NOT NULL AND s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   OR (s.doc_iban_core_ns IS NOT NULL AND s.tx_iban_counterparty_core_ns IS NOT NULL AND s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              OR (s.document_merchant_id IS NOT NULL AND s.transaction_merchant_id IS NOT NULL)
            ))
          ELSE
            (
                 (s.doc_merchant_ns IS NOT NULL AND s.tx_merchant_ns IS NOT NULL)
              OR ( (s.doc_iban_ns IS NOT NULL AND s.tx_iban_counterparty_ns IS NOT NULL AND s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   OR (s.doc_iban_core_ns IS NOT NULL AND s.tx_iban_counterparty_core_ns IS NOT NULL AND s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              OR (s.document_merchant_id IS NOT NULL AND s.transaction_merchant_id IS NOT NULL)
            )
        END
      ) AS merchant_applicable,

      (
        CASE
          WHEN v_directional_matching THEN
            ( s.tx_is_in AND (
                 (s.doc_customer_ns IS NOT NULL AND s.tx_customer_ns IS NOT NULL)
              OR ( (s.doc_iban_ns IS NOT NULL AND s.tx_iban_beneficiary_ns IS NOT NULL AND s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   OR (s.doc_iban_core_ns IS NOT NULL AND s.tx_iban_beneficiary_core_ns IS NOT NULL AND s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            ))
          ELSE
            (
                 (s.doc_customer_ns IS NOT NULL AND s.tx_customer_ns IS NOT NULL)
              OR ( (s.doc_iban_ns IS NOT NULL AND s.tx_iban_beneficiary_ns IS NOT NULL AND s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   OR (s.doc_iban_core_ns IS NOT NULL AND s.tx_iban_beneficiary_core_ns IS NOT NULL AND s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            )
        END
      ) AS customer_applicable

    FROM scored_components s
  ),

  scored_with_confidence AS (
    SELECT
      s.*,

      (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (CASE WHEN s.merchant_applicable THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN s.customer_applicable THEN s.customer_weight ELSE 0 END)
      )::numeric AS dynamic_total_weight,

      (
        s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
        + (CASE WHEN s.merchant_applicable THEN s.merchant_score ELSE 0 END)
        + (CASE WHEN s.customer_applicable THEN s.customer_score ELSE 0 END)
      )::numeric AS total_numerator,

      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_number_match::int,0)
     + COALESCE(s.order_number_match::int,0)
     + COALESCE(s.date_match::int,0)
     + COALESCE(s.currency_match::int,0)
     + COALESCE(s.merchant_match::int,0)
     + COALESCE(s.customer_match::int,0)) AS signals_matched_count,

      CASE WHEN (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (CASE WHEN s.merchant_applicable THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN s.customer_applicable THEN s.customer_weight ELSE 0 END)
      ) > 0 THEN
        ROUND(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
              + (CASE WHEN s.merchant_applicable THEN s.merchant_score ELSE 0 END)
              + (CASE WHEN s.customer_applicable THEN s.customer_score ELSE 0 END))
            / (s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
              + (CASE WHEN s.merchant_applicable THEN s.merchant_weight ELSE 0 END)
              + (CASE WHEN s.customer_applicable THEN s.customer_weight ELSE 0 END))) * 100.0, 4)
      ELSE 0 END AS confidence_percent
    FROM scored_with_applicability s
  ),

  low_signal AS (SELECT * FROM scored_with_confidence WHERE signals_matched_count < 1),
  below_min_confidence AS (
    SELECT * FROM scored_with_confidence
    WHERE signals_matched_count >= 1 AND confidence_percent < v_config_min_confidence
  ),
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE signals_matched_count >= 1 AND confidence_percent >= v_config_min_confidence
  ),

  ranked_per_transaction AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        PARTITION BY b.transaction_id
        ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
      ) AS rownum_per_transaction
    FROM base_candidates b
  ),
  not_top_for_transaction AS (SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction > 1),
  top_per_transaction AS (SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction = 1),

  ranked_per_document AS (
    SELECT
      t.*,
      ROW_NUMBER() OVER (
        PARTITION BY t.doc_dedup_key
        ORDER BY
          t.confidence_percent DESC,
          t.total_numerator DESC,
          t.signals_matched_count DESC,
          ABS(t.transaction_date_unix - t.document_date_unix) ASC,
          t.document_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  not_top_for_document AS (SELECT * FROM ranked_per_document WHERE rownum_per_document > 1),
  top_per_document AS (SELECT * FROM ranked_per_document WHERE rownum_per_document = 1),

  final_candidates AS (
    SELECT *
    FROM top_per_document
    ORDER BY confidence_percent DESC, total_numerator DESC, signals_matched_count DESC, transaction_id, document_id
  ),

  allowed_for_insert AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE  NOT EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       AND NOT EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),
  inserted_reconciliations AS (
    INSERT INTO public.reconciliations (
      transaction_id, document_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary, note
    )
    WITH ts_base AS (
      SELECT f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             COALESCE(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match AS invoice_match,
        d.order_number_match   AS order_match,
        d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,

        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,

        d.amount_score, d.amount_weight,
        d.date_score,   d.date_weight,

        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm, a.tx_order_norm, a.doc_order_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw, a.doc_order_raw,

        a.tx_iban_beneficiary_ns, a.tx_iban_counterparty_ns, a.doc_iban_ns,
        a.tx_iban_beneficiary_core_ns, a.tx_iban_counterparty_core_ns, a.doc_iban_core_ns,

        a.tx_is_in, a.tx_is_out
      FROM ts_base t
      JOIN final_candidates d ON d.transaction_id = t.transaction_id AND d.document_id = t.document_id
      JOIN attributes a       ON a.transaction_id = d.transaction_id AND a.document_id = d.document_id
    ),
    
    notes AS (
      SELECT
        e.*,
        /* % difference between payment and document (2 dp) */
        CASE
          WHEN e.document_total IS NOT NULL AND e.document_total <> 0
          THEN ROUND(ABS(e.transaction_effective_amount - e.document_total) / NULLIF(e.document_total,0) * 100.0, 2)
          ELSE NULL
        END AS amt_diff_pct,

        /* raw time gaps */
        CASE
          WHEN e.transaction_date_unix IS NOT NULL AND e.document_date_unix IS NOT NULL
          THEN ABS(e.transaction_date_unix - e.document_date_unix)
          ELSE NULL
        END AS sec_diff,

        /* derived gaps */
        CASE WHEN e.transaction_date_unix IS NOT NULL AND e.document_date_unix IS NOT NULL
          THEN ABS(e.transaction_date_unix - e.document_date_unix) / 86400.0
          ELSE NULL END AS day_gap,
        CASE WHEN e.transaction_date_unix IS NOT NULL AND e.document_date_unix IS NOT NULL
          THEN ABS(e.transaction_date_unix - e.document_date_unix) / 3600.0
          ELSE NULL END AS hour_gap,
        CASE WHEN e.transaction_date_unix IS NOT NULL AND e.document_date_unix IS NOT NULL
          THEN ABS(e.transaction_date_unix - e.document_date_unix) / 60.0
          ELSE NULL END AS minute_gap,

        /* ratios shown in notes: amount 2 dp; date integer % */
        CASE WHEN e.amount_weight > 0 THEN ROUND((e.amount_score / e.amount_weight) * 100.0, 2) END AS amount_ratio_pct,
        CASE WHEN e.date_weight   > 0 THEN ROUND((e.date_score   / e.date_weight)   * 100.0, 0)  END AS date_ratio_pct
      FROM enrich e
    ),

    notes_text AS (
      SELECT
        n.*,  
        CASE
          WHEN n.date_weight > 0 AND n.date_ratio_pct <= 99 THEN
            TRIM(BOTH ' ' FROM CONCAT_WS(' ',
              /* amount note shown only if amount was penalized */
              CASE
                WHEN n.amount_weight > 0 AND n.amount_score < n.amount_weight AND n.amt_diff_pct IS NOT NULL
                THEN CONCAT('Amount scored ', COALESCE(n.amount_ratio_pct::text,'0'),
                            '% because payment vs document differed by ', n.amt_diff_pct::text, '%.')
              END,
              /* date penalized: round days to 2 dp */
              CASE
                WHEN n.day_gap IS NOT NULL
                THEN CONCAT('Date scored ', COALESCE(n.date_ratio_pct::text,'0'),
                            '% because dates were ', ROUND(n.day_gap::numeric, 2)::text, ' days apart.')
              END
            ))
          ELSE NULL
        END AS note_text
      FROM notes n
    )
    SELECT
      e.transaction_id, e.document_id, e.document_company_id,
      COALESCE(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      EXTRACT(YEAR FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          SELECT json_agg(s) FROM unnest(ARRAY[
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]) s WHERE s IS NOT NULL
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  COALESCE(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_order',     e.tx_order_norm,
        'doc_order',    COALESCE(e.doc_order_raw, e.doc_order_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_iban_beneficiary',        e.tx_iban_beneficiary_ns,
        'tx_iban_counterparty',       e.tx_iban_counterparty_ns,
        'doc_iban',                   e.doc_iban_ns,
        'tx_iban_beneficiary_core',   e.tx_iban_beneficiary_core_ns,
        'tx_iban_counterparty_core',  e.tx_iban_counterparty_core_ns,
        'doc_iban_core',              e.doc_iban_core_ns,
        'tx_is_in',    e.tx_is_in,
        'tx_is_out',   e.tx_is_out,
        'direction',   CASE WHEN e.tx_is_in THEN 'incoming' WHEN e.tx_is_out THEN 'outgoing' ELSE 'unknown' END,
        'days_between_tx_and_doc',
          CASE
            WHEN e.transaction_date_unix IS NOT NULL AND e.document_date_unix IS NOT NULL
              THEN (ABS(e.transaction_date_unix - e.document_date_unix) / 86400)::int
            ELSE NULL
          END
      )::text,
      NULLIF(nt.note_text,'')  -- note
    FROM notes_text nt
    JOIN enrich e USING (transaction_id, document_id)
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score,  invoice_score,  order_score,  merchant_score,  amount_score,  currency_score,  customer_score
    )
    SELECT
      r.reconciliation_id,
      d.document_company_id AS company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      d.order_number_match,
      d.merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      d.order_weight,
      d.merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      d.order_score,
      d.merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    FROM inserted_reconciliations r
    JOIN scored_with_confidence d
      ON d.transaction_id = r.transaction_id
     AND d.document_id    = r.document_id
    RETURNING 1
  ),

  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM top_per_transaction)      AS top_per_transaction_count,
      (SELECT COUNT(*) FROM top_per_document)         AS top_per_document_count,
      (SELECT COUNT(*) FROM final_candidates)         AS passed_confidence_count,
      (SELECT COUNT(*) FROM inserted_reconciliations) AS inserted_count,
      (SELECT COUNT(*) FROM base_candidates)          AS candidates_count,
      (SELECT COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb) FROM seed_counts_total)    AS seed_counts_total_json,
      (SELECT COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb) FROM seed_counts_distinct) AS seed_counts_distinct_json,
      (SELECT COUNT(*) FROM transactions_scope)       AS total_tx_count,
      (SELECT COUNT(*) FROM document_scope)           AS total_doc_count
  )
  SELECT
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  INTO
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  FROM final_counts;

  -- end log
  v_elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - v_start_time)) * 1000)::bigint;

  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    v_inserted_count, NULL, NULL, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered',  v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic',  v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  RETURN jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered',  v_total_docs,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic',  v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

EXCEPTION WHEN OTHERS THEN
  v_elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - v_start_time)) * 1000)::bigint;

  GET STACKED DIAGNOSTICS
    v_err_state   = RETURNED_SQLSTATE,
    v_err_msg     = MESSAGE_TEXT,
    v_err_detail  = PG_EXCEPTION_DETAIL,
    v_err_hint    = PG_EXCEPTION_HINT,
    v_err_context = PG_EXCEPTION_CONTEXT;

  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, v_elapsed_ms,
    jsonb_build_object(
      'sqlstate', v_err_state,
      'detail',   v_err_detail,
      'hint',     v_err_hint,
      'context',  v_err_context
    )
  );

  RETURN jsonb_build_object(
    'msg','error','time_ms',v_elapsed_ms,
    'transactions_scanned',0,'documents_considered',0,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',v_err_msg,'sqlstate',v_err_state
  );
END;
$$;

alter function athena_reconcile_general_v1_9_n(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v1_9_n(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v1_9_n(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v1_9_n(unknown, unknown) to service_role;

create or replace function athena_reconcile_general_invoices_v2(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
declare
  v_status_message text := 'success';
  v_engine_version text := '1.9M-invoices';
  v_start_time timestamptz := clock_timestamp();
  v_elapsed_ms bigint := 0;
  v_config_amount_tolerance numeric;
  v_config_date_window_days numeric;
  v_config_min_confidence numeric;
  v_config_allow_document_duplicates boolean;
  v_config_log_activity boolean;
  v_amount_tolerance_fraction numeric;
  v_date_window_seconds bigint;
  v_date_full_weight_grace_days int := 90;
  v_edge_weight_small constant numeric := 0.10;
  v_gauss_k constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small));
  v_cap_precise int := 5;
  v_cap_broad int := 10;
  v_cap_window_only int := 3;
  v_max_transactions_to_scan int := 20000;
  v_weights_json jsonb := '{}'::jsonb;
  v_weights_total_possible numeric := 0;
  v_summary_top_per_tx bigint := 0;
  v_summary_top_per_doc bigint := 0;
  v_summary_passed_count bigint := 0;
  v_inserted_count bigint := 0;
  v_candidates_count bigint := 0;
  v_total_tx bigint := 0;
  v_total_docs bigint := 0;
  v_seed_counts_total jsonb := '{}'::jsonb;
  v_seed_counts_distinct jsonb := '{}'::jsonb;
  v_log_run_id uuid := gen_random_uuid();
  v_log_run_unix_time bigint := extract(epoch from now())::bigint;
  v_directional_matching boolean := true;
  v_err_state text;
  v_err_msg text;
  v_err_detail text;
  v_err_hint text;
  v_err_context text;
begin
  select amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  into   v_config_amount_tolerance, v_config_date_window_days, v_config_min_confidence,
         v_config_allow_document_duplicates, v_config_log_activity
  from   public.reconciliations_config
  where  reconciliation_config_id = p_reconciliation_config_id;

  if not found then
    v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
    return jsonb_build_object('msg','reconciliations_config not found','time_ms',v_elapsed_ms,
                              'transactions_scanned',0,'documents_considered',0,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  end if;

  v_amount_tolerance_fraction := case when v_config_amount_tolerance >= 1 then v_config_amount_tolerance / 100.0 else v_config_amount_tolerance end;
  v_date_window_seconds := ceil(coalesce(v_config_date_window_days, 1) * 86400)::bigint;
  if v_config_min_confidence is null then v_config_min_confidence := 0; end if;
  if v_config_min_confidence <= 1 then v_config_min_confidence := v_config_min_confidence * 100.0; end if;

  with candidate_weights as (
    select lower(trim(w.weight_key)) as k_norm, (w.weight_value)::numeric as v_norm, w.company_id
    from public.reconciliations_weight_registry w
    where w.active
      and w.merchant_id is null
      and ((p_company_id is null and w.company_id is null) or (p_company_id is not null and (w.company_id = p_company_id or w.company_id is null)))
      and lower(trim(w.weight_key)) in ('amount_total','invoice_number','date','currency','customer_name')
  ),
  picked as (
    select distinct on (k_norm) k_norm, v_norm
    from candidate_weights
    order by k_norm, (company_id is null)
  )
  select coalesce(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) into v_weights_json from picked;

  select coalesce((v_weights_json->>'amount_total')::numeric,0)
       + coalesce((v_weights_json->>'invoice_number')::numeric,0)
       + coalesce((v_weights_json->>'date')::numeric,0)
       + coalesce((v_weights_json->>'currency')::numeric,0)
       + coalesce((v_weights_json->>'customer_name')::numeric,0)
  into v_weights_total_possible;

  if v_weights_total_possible = 0 then
    v_weights_json := jsonb_build_object('amount_total',1,'invoice_number',1,'date',1,'currency',1,'customer_name',1);
    v_weights_total_possible := 5;
  end if;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  ) values (
    v_log_run_id, v_engine_version, 'run_start', 'function', 'ok', null,
    p_company_id, null, null, null,
    null, null, null, null,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', v_config_log_activity,
        'allow_doc_dups', v_config_allow_document_duplicates,
        'amount_tol_pct', v_amount_tolerance_fraction,
        'min_confidence', v_config_min_confidence,
        'date_window_days', v_config_date_window_days,
        'max_tx_seed', v_max_transactions_to_scan,
        'cap_precise', v_cap_precise,
        'cap_broad',   v_cap_broad,
        'cap_window_only', v_cap_window_only,
        'directional_matching', v_directional_matching,
        'gauss_edge_weight', v_edge_weight_small
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  with
  invoice_scope as (
    select
      i.invoice_id as document_id,
      i.author_company_id as company_id_text,
      coalesce(case when i.author_company_id ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' then i.author_company_id::uuid end, p_company_id) as company_id_uuid,
      i.total as total,
      i.currency as currency,
      nullif(i.invoice_nr,'') as invoice_number_raw,
      nullif(public.norm_code(i.invoice_nr),'') as invoice_number_norm,
      case when i.issue_date is not null then i.issue_date::bigint when i.due_date is not null then i.due_date::bigint else null end as document_date_unix,
      nullif(public.norm_text_ns(concat_ws(' ', i.recipient_name, i.recipient_company, i.recipient_first_name, i.recipient_last_name, i.recipient_email, i.recipient_phone)), '') as doc_customer_ns,
      coalesce(i.recipient_name, i.recipient_company, concat_ws(' ', i.recipient_first_name, i.recipient_last_name), i.recipient_email) as doc_customer_display,
      coalesce(i.invoice_id::text, i.id::text) as doc_dedup_key
    from public.invoices i
    where (p_company_id is null or i.author_company_id = p_company_id::text)
      and i.reconciliation_id is null
  ),

  transactions_scope as (
    select
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      nullif(public.norm_code(coalesce(nullif(t.invoice_nr,''), (select m[1] from regexp_matches(upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)), '(INV(?:OICE)?[ -_/]?[A-Z0-9._-]{1,24}|FACTUUR[ -_/]?[A-Z0-9._-]{1,24}|[0-9]{5,10}[-/][A-Z0-9]{1,10})', 'g') as m limit 1))), '') as invoice_nr_norm,
      public.norm_code(upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description))) as tx_text_code,
      t.completed_date,
      (nullif(t.credited,0) is not null or nullif(t.money_in,0)  is not null) as tx_is_in,
      (nullif(t.debited,0)  is not null or nullif(t.money_out,0) is not null) as tx_is_out,
      nullif(public.norm_text_ns(coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty)), '') as tx_customer_ns,
      coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty) as tx_customer_display,
      case when t.debited is not null and t.debited > 0 then t.debited when t.credited is not null and t.credited > 0 then t.credited when t.amount is not null then abs(t.amount) else null end as effective_amount
    from public.transactions t
    where (p_company_id is null or t.company_id = p_company_id::text)
      and t.reconciled is not true
      and (t.invoice_nr is not null
        or (t.currency is not null and (t.name_consumer is not null or t.account_holder_name is not null or t.account_holder_name_counterparty is not null))
        or (t.currency is not null and (t.amount is not null or t.debited is not null or t.credited is not null)))
    order by t.completed_date desc, t.transaction_id
    limit v_max_transactions_to_scan
  ),

  s1_invoice as (
    select t.transaction_id, d.document_id, 'invoice_number' as seed
    from transactions_scope t
    join invoice_scope d
      on d.company_id_text = t.company_id
     and d.currency = t.currency
     and d.invoice_number_norm is not null
     and ((t.invoice_nr_norm is not null and d.invoice_number_norm = t.invoice_nr_norm)
       or (t.tx_text_code is not null and position(d.invoice_number_norm in t.tx_text_code) > 0))
  ),

  s2_customer_exact_amount as (
    select t.transaction_id, d_sub.document_id, 'customer_ns_exact_amount' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and ((not v_directional_matching) or t.tx_is_in)
        and t.credited is not null and t.credited > 0
        and d.total = t.credited
        and d.doc_customer_ns is not null and t.tx_customer_ns is not null
        and (d.doc_customer_ns = t.tx_customer_ns or position(d.doc_customer_ns in t.tx_customer_ns) > 0 or position(t.tx_customer_ns in d.doc_customer_ns) > 0)
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
      order by d.document_date_unix, d.document_id
      limit v_cap_precise
    ) d_sub on true
    where done.transaction_id is null
  ),

  s3_window_amount_exact as (
    select t.transaction_id, d_sub.document_id, 'window_amount_exact' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
        and (((not v_directional_matching) or t.tx_is_in) and t.credited is not null and t.credited > 0 and d.total = t.credited
          or ((not v_directional_matching) or t.tx_is_out) and t.debited is not null and t.debited > 0 and d.total = t.debited)
      order by d.document_date_unix, d.document_id
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s4_window_amount_tol as (
    select t.transaction_id, d_sub.document_id, 'window_amount_tol' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount union all select distinct transaction_id from s3_window_amount_exact) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
        and v_amount_tolerance_fraction > 0
        and (((not v_directional_matching) or t.tx_is_in) and t.credited is not null and t.credited > 0 and d.total between (t.credited / (1 + v_amount_tolerance_fraction)) and (t.credited / (1 - v_amount_tolerance_fraction))
          or ((not v_directional_matching) or t.tx_is_out) and t.debited is not null and t.debited > 0 and d.total between (t.debited / (1 + v_amount_tolerance_fraction)) and (t.debited / (1 - v_amount_tolerance_fraction)))
      order by d.document_date_unix, d.document_id
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s5_window_only as (
    select t.transaction_id, d_sub.document_id, 'window_only' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount union all select distinct transaction_id from s3_window_amount_exact union all select distinct transaction_id from s4_window_amount_tol) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
      order by d.document_date_unix, d.document_id
      limit v_cap_window_only
    ) d_sub on true
    where done.transaction_id is null
  ),

  seeds as (
    select * from s1_invoice
    union all select * from s2_customer_exact_amount
    union all select * from s3_window_amount_exact
    union all select * from s4_window_amount_tol
    union all select * from s5_window_only
  ),

  seed_counts_total as (select seed, count(*) as ct from seeds group by seed),
  seed_counts_distinct as (select seed, count(distinct (transaction_id, document_id)) as ct from seeds group by seed),

  candidate_pairs as (select distinct transaction_id, document_id from seeds),

  distinct_pairs as (select distinct transaction_id, document_id from candidate_pairs),

  attributes as (
    select
      p.transaction_id,
      p.document_id,
      d.company_id_uuid as document_company_id,
      t.amount as transaction_amount,
      t.debited as transaction_debited,
      t.credited as transaction_credited,
      d.total as document_total,
      t.currency as transaction_currency,
      d.currency as document_currency,
      t.invoice_nr_norm as tx_invoice_norm,
      d.invoice_number_norm as doc_invoice_norm,
      d.invoice_number_raw as doc_invoice_raw,
      t.completed_date as transaction_date_unix,
      d.document_date_unix as document_date_unix,
      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_customer_display as transaction_customer_name,
      d.doc_customer_display as document_customer_name,
      t.tx_text_code,
      coalesce(nullif(d.doc_dedup_key,''), d.document_id::text) as doc_dedup_key,
      t.effective_amount as transaction_effective_amount,
      t.tx_is_in,
      t.tx_is_out
    from distinct_pairs p
    join transactions_scope t on t.transaction_id = p.transaction_id
    join invoice_scope    d on d.document_id    = p.document_id
    where t.company_id = d.company_id_text
  ),

  evaluated as (
    select
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,
      coalesce(((a.tx_invoice_norm is not null and a.doc_invoice_norm is not null and a.tx_invoice_norm = a.doc_invoice_norm) or (a.tx_text_code is not null and a.doc_invoice_norm is not null and position(a.doc_invoice_norm in a.tx_text_code) > 0)), false) as invoice_number_match,
      coalesce(a.transaction_currency  = a.document_currency, false) as currency_match,
      (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (a.tx_customer_ns = a.doc_customer_ns or position(a.doc_customer_ns in a.tx_customer_ns) > 0 or position(a.tx_customer_ns in a.doc_customer_ns) > 0)) as customer_match,
      coalesce(abs(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, false) as date_within_cap,
      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,
      (a.transaction_debited is not null and a.transaction_debited > 0 and a.document_total = a.transaction_debited) as amount_exact_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and a.document_total = a.transaction_credited) as amount_exact_from_credited,
      (a.transaction_debited is not null and a.transaction_debited > 0 and abs(a.document_total - a.transaction_debited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and abs(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_credited,
      a.tx_is_in,
      a.tx_is_out,
      (a.transaction_credited is not null and a.transaction_credited > 0) as credited_pos,
      (a.transaction_debited is not null and a.transaction_debited > 0) as debited_pos
    from attributes a
  ),

  with_amount_tolerance as (
    select
      e.*,
      case when v_directional_matching then (e.tx_is_out and e.amount_exact_from_debited) or (e.tx_is_in and e.amount_exact_from_credited) else (e.amount_exact_from_debited or e.amount_exact_from_credited) end as amount_exact_match,
      case when v_amount_tolerance_fraction > 0 then case when v_directional_matching then (e.tx_is_out and e.amount_tolerance_from_debited) or (e.tx_is_in and e.amount_tolerance_from_credited) else (e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) end else false end as amount_tolerance_match
    from evaluated e
  ),

  resolved_weights as (
    select
      coalesce((v_weights_json->>'amount_total')::numeric, 0) as amount_weight,
      coalesce((v_weights_json->>'invoice_number')::numeric, 0) as invoice_weight,
      coalesce((v_weights_json->>'date')::numeric, 0) as date_weight,
      coalesce((v_weights_json->>'currency')::numeric, 0) as currency_weight,
      coalesce((v_weights_json->>'customer_name')::numeric, 0) as customer_weight
  ),

  scored_components as (
    select
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,
      m.transaction_date_unix,
      m.document_date_unix,
      m.tx_is_in, m.tx_is_out,
      coalesce(case when m.amount_exact_match then w.amount_weight when m.amount_tolerance_match then w.amount_weight * exp(-0.5 * power((abs(m.transaction_effective_amount - m.document_total)) / nullif((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0), 2)) else 0 end, 0)::numeric as amount_score,
      (m.amount_exact_match or m.amount_tolerance_match) as amount_match,
      m.invoice_number_match as invoice_number_match,
      case when m.transaction_date_unix is not null and m.document_date_unix is not null then w.date_weight * exp(-0.5 * power(greatest(0, (abs(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days) / nullif(greatest(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k), 0), 2)) else 0 end::numeric as date_score,
      m.date_within_cap as date_match,
      m.currency_match as currency_match,
      m.customer_match as customer_match,
      w.amount_weight as amount_weight,
      w.invoice_weight as invoice_weight,
      w.date_weight as date_weight,
      w.currency_weight as currency_weight,
      w.customer_weight as customer_weight,
      (coalesce(m.invoice_number_match::int,0) * w.invoice_weight)::numeric as invoice_score,
      (coalesce(m.currency_match::int,0) * w.currency_weight)::numeric as currency_score,
      (coalesce(m.customer_match::int,0) * w.customer_weight)::numeric as customer_score,
      m.transaction_effective_amount,
      m.document_total
    from with_amount_tolerance m
    cross join resolved_weights w
  ),

  scored_with_confidence as (
    select
      s.*,
      (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight)::numeric as dynamic_total_weight,
      (s.amount_score + s.invoice_score + s.date_score + s.currency_score + s.customer_score)::numeric as total_numerator,
      (coalesce(s.amount_match::int,0) + coalesce(s.invoice_number_match::int,0) + coalesce(s.date_match::int,0) + coalesce(s.currency_match::int,0) + coalesce(s.customer_match::int,0)) as signals_matched_count,
      case when (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight) > 0 then round(((s.amount_score + s.invoice_score + s.date_score + s.currency_score + s.customer_score) / (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight)) * 100.0, 4) else 0 end as confidence_percent
    from scored_components s
  ),

  low_signal as (select * from scored_with_confidence where signals_matched_count < 1),
  below_min_confidence as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent < v_config_min_confidence),
  base_candidates as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent >= v_config_min_confidence),

  ranked_per_transaction as (
    select b.*, row_number() over (partition by b.transaction_id order by b.confidence_percent desc, b.total_numerator desc, b.signals_matched_count desc, b.document_id) as rownum_per_transaction
    from base_candidates b
  ),
  not_top_for_transaction as (select * from ranked_per_transaction where rownum_per_transaction > 1),
  top_per_transaction as (select * from ranked_per_transaction where rownum_per_transaction = 1),

  ranked_per_document as (
    select t.*, row_number() over (partition by t.doc_dedup_key order by t.confidence_percent desc, t.total_numerator desc, t.signals_matched_count desc, abs(t.transaction_date_unix - t.document_date_unix) asc, t.document_id) as rownum_per_document
    from top_per_transaction t
  ),
  not_top_for_document as (select * from ranked_per_document where rownum_per_document > 1),
  top_per_document as (select * from ranked_per_document where rownum_per_document = 1),

  final_candidates as (
    select * from top_per_document
    order by confidence_percent desc, total_numerator desc, signals_matched_count desc, transaction_id, document_id
  ),

  allowed_for_insert as (
    select f.*
    from   final_candidates f
    where  not exists (select 1 from public.reconciliations r1 where r1.transaction_id = f.transaction_id)
       and not exists (select 1 from public.reconciliations r2 where r2.invoice_id    = f.document_id)
  ),

  inserted_reconciliations as (
    insert into public.reconciliations (
      transaction_id, invoice_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary, note
    )
    with ts_base as (
      select f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             coalesce(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint as base_unix
      from allowed_for_insert f
    ),
    enrich as (
      select
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match as invoice_match,
        d.amount_match, d.date_match, d.currency_match, d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        d.amount_score, d.amount_weight,
        d.date_score,   d.date_weight,
        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw,
        (a.tx_is_in or e.credited_pos) as tx_is_in_final,
        (a.tx_is_out or e.debited_pos) as tx_is_out_final
      from ts_base t
      join final_candidates d on d.transaction_id = t.transaction_id and d.document_id = t.document_id
      join evaluated        e on e.transaction_id = d.transaction_id and e.document_id = d.document_id
      join attributes       a on a.transaction_id = d.transaction_id and a.document_id = d.document_id
    ),
    notes as (
      select
        e.*,
        case when e.document_total is not null and e.document_total <> 0 then round(abs(e.transaction_effective_amount - e.document_total) / nullif(e.document_total,0) * 100.0, 2) else null end as amt_diff_pct,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) else null end as sec_diff,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) / 86400.0 else null end as day_gap,
        case when e.amount_weight > 0 then round((e.amount_score / e.amount_weight) * 100.0, 2) end as amount_ratio_pct,
        case when e.date_weight   > 0 then round((e.date_score   / e.date_weight)   * 100.0, 0)  end as date_ratio_pct
      from enrich e
    ),
    notes_text as (
      select
        n.*,
        case
          when n.date_weight > 0 and n.date_ratio_pct <= 99 then
            trim(both ' ' from concat_ws(' ',
              case when n.amount_weight > 0 and n.amount_score < n.amount_weight and n.amt_diff_pct is not null then concat('Amount scored ', coalesce(n.amount_ratio_pct::text,'0'), '% because payment vs invoice differed by ', n.amt_diff_pct::text, '%.') end,
              case when n.day_gap is not null then concat('Date scored ', coalesce(n.date_ratio_pct::text,'0'), '% because dates were ', round(n.day_gap::numeric, 2)::text, ' days apart.') end
            ))
          else null
        end as note_text
      from notes n
    )
    select
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      coalesce(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      extract(year from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      extract(quarter from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) at time zone 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          select json_agg(s) from unnest(array[
            case when e.invoice_match  then 'invoice'  end,
            case when e.amount_match   then 'amount'   end,
            case when e.date_match     then 'date'     end,
            case when e.currency_match then 'currency' end,
            case when e.customer_match then 'customer' end
          ]) s where s is not null
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  coalesce(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_is_in',     e.tx_is_in_final,
        'tx_is_out',    e.tx_is_out_final,
        'direction',    case when e.tx_is_in_final then 'incoming' when e.tx_is_out_final then 'outgoing' else 'unknown' end,
        'days_between_tx_and_doc',
          case when e.transaction_date_unix is not null and e.document_date_unix is not null then (abs(e.transaction_date_unix - e.document_date_unix) / 86400)::int else null end
      )::text,
      nullif(nt.note_text,'')
    from notes_text nt
    join enrich e using (transaction_id, document_id)
    returning reconciliation_id, transaction_id, invoice_id, confidence
  ),

  inserted_match_scores as (
    insert into public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score,  invoice_score,  order_score,  merchant_score,  amount_score,  currency_score,  customer_score
    )
    select
      r.reconciliation_id,
      d.document_company_id as company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      false as order_match,
      false as merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      0 as order_weight,
      0 as merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      0 as order_score,
      0 as merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    from inserted_reconciliations r
    join scored_with_confidence d
      on d.transaction_id = r.transaction_id
     and d.document_id    = r.invoice_id
    returning 1
  ),

  final_counts as (
    select
      (select count(*) from top_per_transaction) as top_per_transaction_count,
      (select count(*) from top_per_document) as top_per_document_count,
      (select count(*) from final_candidates) as passed_confidence_count,
      (select count(*) from inserted_reconciliations) as inserted_count,
      (select count(*) from base_candidates) as candidates_count,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_total) as seed_counts_total_json,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_distinct) as seed_counts_distinct_json,
      (select count(*) from transactions_scope) as total_tx_count,
      (select count(*) from invoice_scope) as total_doc_count
  )
  select
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  into
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  from final_counts;

  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', null,
    p_company_id, null, null, null,
    v_inserted_count, null, null, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered',  v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic',  v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  return jsonb_build_object(
    'msg', v_status_message,
    'time_ms', v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered',  v_total_docs,
    'top_per_tx', v_summary_top_per_tx,
    'top_per_doc', v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic',  v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

exception when others then
  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
  get stacked diagnostics
    v_err_state   = returned_sqlstate,
    v_err_msg     = message_text,
    v_err_detail  = pg_exception_detail,
    v_err_hint    = pg_exception_hint,
    v_err_context = pg_exception_context;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg,
    p_company_id, null, null, null,
    null, null, null, v_elapsed_ms,
    jsonb_build_object('sqlstate', v_err_state, 'detail', v_err_detail, 'hint', v_err_hint, 'context', v_err_context)
  );

  return jsonb_build_object(
    'msg','error','time_ms',v_elapsed_ms,
    'transactions_scanned',0,'documents_considered',0,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',v_err_msg,'sqlstate',v_err_state
  );
end;
$$;

alter function athena_reconcile_general_invoices_v2(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_invoices_v2(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_invoices_v2(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_invoices_v2(unknown, unknown) to service_role;

create or replace function athena_reconcile_general_invoices_latest(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
declare
  v_status_message text := 'success';
  v_engine_version text := '1.9M-invoices';
  v_start_time timestamptz := clock_timestamp();
  v_elapsed_ms bigint := 0;
  v_config_amount_tolerance numeric;
  v_config_date_window_days numeric;
  v_config_min_confidence numeric;
  v_config_allow_document_duplicates boolean;
  v_config_log_activity boolean;
  v_amount_tolerance_fraction numeric;
  v_date_window_seconds bigint;
  v_date_full_weight_grace_days int := 30;
  v_edge_weight_small constant numeric := 0.10;
  v_gauss_k constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small));
  v_cap_precise int := 5;
  v_cap_broad int := 10;
  v_cap_window_only int := 3;
  v_max_transactions_to_scan int := 20000;
  v_weights_json jsonb := '{}'::jsonb;
  v_weights_total_possible numeric := 0;
  v_summary_top_per_tx bigint := 0;
  v_summary_top_per_doc bigint := 0;
  v_summary_passed_count bigint := 0;
  v_inserted_count bigint := 0;
  v_candidates_count bigint := 0;
  v_total_tx bigint := 0;
  v_total_docs bigint := 0;
  v_seed_counts_total jsonb := '{}'::jsonb;
  v_seed_counts_distinct jsonb := '{}'::jsonb;
  v_log_run_id uuid := gen_random_uuid();
  v_log_run_unix_time bigint := extract(epoch from now())::bigint;
  v_directional_matching boolean := true;
  v_err_state text;
  v_err_msg text;
  v_err_detail text;
  v_err_hint text;
  v_err_context text;
begin
  select amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  into   v_config_amount_tolerance, v_config_date_window_days, v_config_min_confidence,
         v_config_allow_document_duplicates, v_config_log_activity
  from   public.reconciliations_config
  where  reconciliation_config_id = p_reconciliation_config_id;

  if not found then
    v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
    return jsonb_build_object('msg','reconciliations_config not found','time_ms',v_elapsed_ms,
                              'transactions_scanned',0,'documents_considered',0,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  end if;

  v_amount_tolerance_fraction := case when v_config_amount_tolerance >= 1 then v_config_amount_tolerance / 100.0 else v_config_amount_tolerance end;
  v_date_window_seconds := ceil(coalesce(v_config_date_window_days, 1) * 86400)::bigint;
  if v_config_min_confidence is null then v_config_min_confidence := 0; end if;
  if v_config_min_confidence <= 1 then v_config_min_confidence := v_config_min_confidence * 100.0; end if;

  with candidate_weights as (
    select lower(trim(w.weight_key)) as k_norm, (w.weight_value)::numeric as v_norm, w.company_id
    from public.reconciliations_weight_registry w
    where w.active
      and w.merchant_id is null
      and ((p_company_id is null and w.company_id is null) or (p_company_id is not null and (w.company_id = p_company_id or w.company_id is null)))
      and lower(trim(w.weight_key)) in ('amount_total','invoice_number','date','currency','customer_name')
  ),
  picked as (
    select distinct on (k_norm) k_norm, v_norm
    from candidate_weights
    order by k_norm, (company_id is null)
  )
  select coalesce(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) into v_weights_json from picked;

  select coalesce((v_weights_json->>'amount_total')::numeric,0)
       + coalesce((v_weights_json->>'invoice_number')::numeric,0)
       + coalesce((v_weights_json->>'date')::numeric,0)
       + coalesce((v_weights_json->>'currency')::numeric,0)
       + coalesce((v_weights_json->>'customer_name')::numeric,0)
  into v_weights_total_possible;

  if v_weights_total_possible = 0 then
    v_weights_json := jsonb_build_object('amount_total',1,'invoice_number',1,'date',1,'currency',1,'customer_name',1);
    v_weights_total_possible := 5;
  end if;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  ) values (
    v_log_run_id, v_engine_version, 'run_start', 'function', 'ok', null,
    p_company_id, null, null, null,
    null, null, null, null,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', v_config_log_activity,
        'allow_doc_dups', v_config_allow_document_duplicates,
        'amount_tol_pct', v_amount_tolerance_fraction,
        'min_confidence', v_config_min_confidence,
        'date_window_days', v_config_date_window_days,
        'max_tx_seed', v_max_transactions_to_scan,
        'cap_precise', v_cap_precise,
        'cap_broad',   v_cap_broad,
        'cap_window_only', v_cap_window_only,
        'directional_matching', v_directional_matching,
        'gauss_edge_weight', v_edge_weight_small
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  with
  invoice_scope as (
    select
      i.invoice_id as document_id,
      i.author_company_id as company_id_text,
      coalesce(case when i.author_company_id ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' then i.author_company_id::uuid end, p_company_id) as company_id_uuid,
      i.total as total,
      i.currency as currency,
      nullif(i.invoice_nr,'') as invoice_number_raw,
      nullif(public.norm_code(i.invoice_nr),'') as invoice_number_norm,
      case when i.issue_date is not null then i.issue_date::bigint when i.due_date is not null then i.due_date::bigint else null end as document_date_unix,
      nullif(public.norm_text_ns(concat_ws(' ', i.recipient_name, i.recipient_company, i.recipient_first_name, i.recipient_last_name, i.recipient_email, i.recipient_phone)), '') as doc_customer_ns,
      coalesce(i.recipient_name, i.recipient_company, concat_ws(' ', i.recipient_first_name, i.recipient_last_name), i.recipient_email) as doc_customer_display,
      coalesce(i.invoice_id::text, i.id::text) as doc_dedup_key
    from public.invoices i
    where (p_company_id is null or i.author_company_id = p_company_id::text)
      and i.reconciliation_id is null
  ),

  transactions_scope as (
    select
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      nullif(public.norm_code(coalesce(nullif(t.invoice_nr,''), (select m[1] from regexp_matches(upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)), '(INV(?:OICE)?[ -_/]?[A-Z0-9._-]{1,24}|FACTUUR[ -_/]?[A-Z0-9._-]{1,24}|[0-9]{5,10}[-/][A-Z0-9]{1,10})', 'g') as m limit 1))), '') as invoice_nr_norm,
      public.norm_code(upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description))) as tx_text_code,
      t.completed_date,
      (nullif(t.credited,0) is not null or nullif(t.money_in,0)  is not null) as tx_is_in,
      (nullif(t.debited,0)  is not null or nullif(t.money_out,0) is not null) as tx_is_out,
      nullif(public.norm_text_ns(coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty)), '') as tx_customer_ns,
      coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty) as tx_customer_display,
      case when t.debited is not null and t.debited > 0 then t.debited when t.credited is not null and t.credited > 0 then t.credited when t.amount is not null then abs(t.amount) else null end as effective_amount
    from public.transactions t
    where (p_company_id is null or t.company_id = p_company_id::text)
      and t.reconciled is not true
      and (t.invoice_nr is not null
        or (t.currency is not null and (t.name_consumer is not null or t.account_holder_name is not null or t.account_holder_name_counterparty is not null))
        or (t.currency is not null and (t.amount is not null or t.debited is not null or t.credited is not null)))
    order by t.completed_date desc, t.transaction_id
    limit v_max_transactions_to_scan
  ),

  s1_invoice as (
    select t.transaction_id, d.document_id, 'invoice_number' as seed
    from transactions_scope t
    join invoice_scope d
      on d.company_id_text = t.company_id
     and d.currency = t.currency
     and d.invoice_number_norm is not null
     and ((t.invoice_nr_norm is not null and d.invoice_number_norm = t.invoice_nr_norm)
       or (t.tx_text_code is not null and position(d.invoice_number_norm in t.tx_text_code) > 0))
  ),

  s2_customer_exact_amount as (
    select t.transaction_id, d_sub.document_id, 'customer_ns_exact_amount' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and ((not v_directional_matching) or t.tx_is_in)
        and t.credited is not null and t.credited > 0
        and d.total = t.credited
        and d.doc_customer_ns is not null and t.tx_customer_ns is not null
        and (d.doc_customer_ns = t.tx_customer_ns or position(d.doc_customer_ns in t.tx_customer_ns) > 0 or position(t.tx_customer_ns in d.doc_customer_ns) > 0)
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
      order by d.document_date_unix, d.document_id
      limit v_cap_precise
    ) d_sub on true
    where done.transaction_id is null
  ),

  s3_window_amount_exact as (
    select t.transaction_id, d_sub.document_id, 'window_amount_exact' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
        and (((not v_directional_matching) or t.tx_is_in) and t.credited is not null and t.credited > 0 and d.total = t.credited
          or ((not v_directional_matching) or t.tx_is_out) and t.debited is not null and t.debited > 0 and d.total = t.debited)
      order by d.document_date_unix, d.document_id
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s4_window_amount_tol as (
    select t.transaction_id, d_sub.document_id, 'window_amount_tol' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount union all select distinct transaction_id from s3_window_amount_exact) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
        and v_amount_tolerance_fraction > 0
        and (((not v_directional_matching) or t.tx_is_in) and t.credited is not null and t.credited > 0 and d.total between (t.credited / (1 + v_amount_tolerance_fraction)) and (t.credited / (1 - v_amount_tolerance_fraction))
          or ((not v_directional_matching) or t.tx_is_out) and t.debited is not null and t.debited > 0 and d.total between (t.debited / (1 + v_amount_tolerance_fraction)) and (t.debited / (1 - v_amount_tolerance_fraction)))
      order by d.document_date_unix, d.document_id
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s5_window_only as (
    select t.transaction_id, d_sub.document_id, 'window_only' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount union all select distinct transaction_id from s3_window_amount_exact union all select distinct transaction_id from s4_window_amount_tol) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
      order by d.document_date_unix, d.document_id
      limit v_cap_window_only
    ) d_sub on true
    where done.transaction_id is null
  ),

  seeds as (
    select * from s1_invoice
    union all select * from s2_customer_exact_amount
    union all select * from s3_window_amount_exact
    union all select * from s4_window_amount_tol
    union all select * from s5_window_only
  ),

  seed_counts_total as (select seed, count(*) as ct from seeds group by seed),
  seed_counts_distinct as (select seed, count(distinct (transaction_id, document_id)) as ct from seeds group by seed),

  candidate_pairs as (select distinct transaction_id, document_id from seeds),

  distinct_pairs as (select distinct transaction_id, document_id from candidate_pairs),

  attributes as (
    select
      p.transaction_id,
      p.document_id,
      d.company_id_uuid as document_company_id,
      t.amount as transaction_amount,
      t.debited as transaction_debited,
      t.credited as transaction_credited,
      d.total as document_total,
      t.currency as transaction_currency,
      d.currency as document_currency,
      t.invoice_nr_norm as tx_invoice_norm,
      d.invoice_number_norm as doc_invoice_norm,
      d.invoice_number_raw as doc_invoice_raw,
      t.completed_date as transaction_date_unix,
      d.document_date_unix as document_date_unix,
      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_customer_display as transaction_customer_name,
      d.doc_customer_display as document_customer_name,
      t.tx_text_code,
      coalesce(nullif(d.doc_dedup_key,''), d.document_id::text) as doc_dedup_key,
      t.effective_amount as transaction_effective_amount,
      t.tx_is_in,
      t.tx_is_out
    from distinct_pairs p
    join transactions_scope t on t.transaction_id = p.transaction_id
    join invoice_scope    d on d.document_id    = p.document_id
    where t.company_id = d.company_id_text
  ),

  evaluated as (
    select
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,
      coalesce(((a.tx_invoice_norm is not null and a.doc_invoice_norm is not null and a.tx_invoice_norm = a.doc_invoice_norm) or (a.tx_text_code is not null and a.doc_invoice_norm is not null and position(a.doc_invoice_norm in a.tx_text_code) > 0)), false) as invoice_number_match,
      coalesce(a.transaction_currency  = a.document_currency, false) as currency_match,
      (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (a.tx_customer_ns = a.doc_customer_ns or position(a.doc_customer_ns in a.tx_customer_ns) > 0 or position(a.tx_customer_ns in a.doc_customer_ns) > 0)) as customer_match,
      coalesce(abs(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, false) as date_within_cap,
      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,
      (a.transaction_debited is not null and a.transaction_debited > 0 and a.document_total = a.transaction_debited) as amount_exact_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and a.document_total = a.transaction_credited) as amount_exact_from_credited,
      (a.transaction_debited is not null and a.transaction_debited > 0 and abs(a.document_total - a.transaction_debited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and abs(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_credited,
      a.tx_is_in,
      a.tx_is_out,
      (a.transaction_credited is not null and a.transaction_credited > 0) as credited_pos,
      (a.transaction_debited is not null and a.transaction_debited > 0) as debited_pos
    from attributes a
  ),

  with_amount_tolerance as (
    select
      e.*,
      case when v_directional_matching then (e.tx_is_out and e.amount_exact_from_debited) or (e.tx_is_in and e.amount_exact_from_credited) else (e.amount_exact_from_debited or e.amount_exact_from_credited) end as amount_exact_match,
      case when v_amount_tolerance_fraction > 0 then case when v_directional_matching then (e.tx_is_out and e.amount_tolerance_from_debited) or (e.tx_is_in and e.amount_tolerance_from_credited) else (e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) end else false end as amount_tolerance_match
    from evaluated e
  ),

  resolved_weights as (
    select
      coalesce((v_weights_json->>'amount_total')::numeric, 0) as amount_weight,
      coalesce((v_weights_json->>'invoice_number')::numeric, 0) as invoice_weight,
      coalesce((v_weights_json->>'date')::numeric, 0) as date_weight,
      coalesce((v_weights_json->>'currency')::numeric, 0) as currency_weight,
      coalesce((v_weights_json->>'customer_name')::numeric, 0) as customer_weight
  ),

  scored_components as (
    select
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,
      m.transaction_date_unix,
      m.document_date_unix,
      m.tx_is_in, m.tx_is_out,
      coalesce(case when m.amount_exact_match then w.amount_weight when m.amount_tolerance_match then w.amount_weight * exp(-0.5 * power((abs(m.transaction_effective_amount - m.document_total)) / nullif((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0), 2)) else 0 end, 0)::numeric as amount_score,
      (m.amount_exact_match or m.amount_tolerance_match) as amount_match,
      m.invoice_number_match as invoice_number_match,
      case when m.transaction_date_unix is not null and m.document_date_unix is not null then w.date_weight * exp(-0.5 * power(greatest(0, (abs(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days) / nullif(greatest(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k), 0), 2)) else 0 end::numeric as date_score,
      m.date_within_cap as date_match,
      m.currency_match as currency_match,
      m.customer_match as customer_match,
      w.amount_weight as amount_weight,
      w.invoice_weight as invoice_weight,
      w.date_weight as date_weight,
      w.currency_weight as currency_weight,
      w.customer_weight as customer_weight,
      (coalesce(m.invoice_number_match::int,0) * w.invoice_weight)::numeric as invoice_score,
      (coalesce(m.currency_match::int,0) * w.currency_weight)::numeric as currency_score,
      (coalesce(m.customer_match::int,0) * w.customer_weight)::numeric as customer_score,
      m.transaction_effective_amount,
      m.document_total
    from with_amount_tolerance m
    cross join resolved_weights w
  ),

  scored_with_confidence as (
    select
      s.*,
      (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight)::numeric as dynamic_total_weight,
      (s.amount_score + s.invoice_score + s.date_score + s.currency_score + s.customer_score)::numeric as total_numerator,
      (coalesce(s.amount_match::int,0) + coalesce(s.invoice_number_match::int,0) + coalesce(s.date_match::int,0) + coalesce(s.currency_match::int,0) + coalesce(s.customer_match::int,0)) as signals_matched_count,
      case when (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight) > 0 then round(((s.amount_score + s.invoice_score + s.date_score + s.currency_score + s.customer_score) / (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight)) * 100.0, 4) else 0 end as confidence_percent
    from scored_components s
  ),

  low_signal as (select * from scored_with_confidence where signals_matched_count < 1),
  below_min_confidence as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent < v_config_min_confidence),
  base_candidates as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent >= v_config_min_confidence),

  ranked_per_transaction as (
    select b.*, row_number() over (partition by b.transaction_id order by b.confidence_percent desc, b.total_numerator desc, b.signals_matched_count desc, b.document_id) as rownum_per_transaction
    from base_candidates b
  ),
  not_top_for_transaction as (select * from ranked_per_transaction where rownum_per_transaction > 1),
  top_per_transaction as (select * from ranked_per_transaction where rownum_per_transaction = 1),

  ranked_per_document as (
    select t.*, row_number() over (partition by t.doc_dedup_key order by t.confidence_percent desc, t.total_numerator desc, t.signals_matched_count desc, abs(t.transaction_date_unix - t.document_date_unix) asc, t.document_id) as rownum_per_document
    from top_per_transaction t
  ),
  not_top_for_document as (select * from ranked_per_document where rownum_per_document > 1),
  top_per_document as (select * from ranked_per_document where rownum_per_document = 1),

  final_candidates as (
    select * from top_per_document
    order by confidence_percent desc, total_numerator desc, signals_matched_count desc, transaction_id, document_id
  ),

  allowed_for_insert as (
    select f.*
    from   final_candidates f
    where  not exists (select 1 from public.reconciliations r1 where r1.transaction_id = f.transaction_id)
       and not exists (select 1 from public.reconciliations r2 where r2.invoice_id    = f.document_id)
  ),

  inserted_reconciliations as (
    insert into public.reconciliations (
      transaction_id, invoice_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary, note
    )
    with ts_base as (
      select f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             coalesce(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint as base_unix
      from allowed_for_insert f
    ),
    enrich as (
      select
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match as invoice_match,
        d.amount_match, d.date_match, d.currency_match, d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        d.amount_score, d.amount_weight,
        d.date_score,   d.date_weight,
        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw,
        (a.tx_is_in or e.credited_pos) as tx_is_in_final,
        (a.tx_is_out or e.debited_pos) as tx_is_out_final
      from ts_base t
      join final_candidates d on d.transaction_id = t.transaction_id and d.document_id = t.document_id
      join evaluated        e on e.transaction_id = d.transaction_id and e.document_id = d.document_id
      join attributes       a on a.transaction_id = d.transaction_id and a.document_id = d.document_id
    ),
    notes as (
      select
        e.*,
        case when e.document_total is not null and e.document_total <> 0 then round(abs(e.transaction_effective_amount - e.document_total) / nullif(e.document_total,0) * 100.0, 2) else null end as amt_diff_pct,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) else null end as sec_diff,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) / 86400.0 else null end as day_gap,
        case when e.amount_weight > 0 then round((e.amount_score / e.amount_weight) * 100.0, 2) end as amount_ratio_pct,
        case when e.date_weight   > 0 then round((e.date_score   / e.date_weight)   * 100.0, 0)  end as date_ratio_pct
      from enrich e
    ),
    notes_text as (
      select
        n.*,
        case
          when n.date_weight > 0 and n.date_ratio_pct <= 99 then
            trim(both ' ' from concat_ws(' ',
              case when n.amount_weight > 0 and n.amount_score < n.amount_weight and n.amt_diff_pct is not null then concat('Amount scored ', coalesce(n.amount_ratio_pct::text,'0'), '% because payment vs invoice differed by ', n.amt_diff_pct::text, '%.') end,
              case when n.day_gap is not null then concat('Date scored ', coalesce(n.date_ratio_pct::text,'0'), '% because dates were ', round(n.day_gap::numeric, 2)::text, ' days apart.') end
            ))
          else null
        end as note_text
      from notes n
    )
    select
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      coalesce(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      extract(year from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      extract(quarter from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) at time zone 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          select json_agg(s) from unnest(array[
            case when e.invoice_match  then 'invoice'  end,
            case when e.amount_match   then 'amount'   end,
            case when e.date_match     then 'date'     end,
            case when e.currency_match then 'currency' end,
            case when e.customer_match then 'customer' end
          ]) s where s is not null
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  coalesce(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_is_in',     e.tx_is_in_final,
        'tx_is_out',    e.tx_is_out_final,
        'direction',    case when e.tx_is_in_final then 'incoming' when e.tx_is_out_final then 'outgoing' else 'unknown' end,
        'days_between_tx_and_doc',
          case when e.transaction_date_unix is not null and e.document_date_unix is not null then (abs(e.transaction_date_unix - e.document_date_unix) / 86400)::int else null end
      )::text,
      nullif(nt.note_text,'')
    from notes_text nt
    join enrich e using (transaction_id, document_id)
    returning reconciliation_id, transaction_id, invoice_id, confidence
  ),

  inserted_match_scores as (
    insert into public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score,  invoice_score,  order_score,  merchant_score,  amount_score,  currency_score,  customer_score
    )
    select
      r.reconciliation_id,
      d.document_company_id as company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      false as order_match,
      false as merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      0 as order_weight,
      0 as merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      0 as order_score,
      0 as merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    from inserted_reconciliations r
    join scored_with_confidence d
      on d.transaction_id = r.transaction_id
     and d.document_id    = r.invoice_id
    returning 1
  ),

  final_counts as (
    select
      (select count(*) from top_per_transaction) as top_per_transaction_count,
      (select count(*) from top_per_document) as top_per_document_count,
      (select count(*) from final_candidates) as passed_confidence_count,
      (select count(*) from inserted_reconciliations) as inserted_count,
      (select count(*) from base_candidates) as candidates_count,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_total) as seed_counts_total_json,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_distinct) as seed_counts_distinct_json,
      (select count(*) from transactions_scope) as total_tx_count,
      (select count(*) from invoice_scope) as total_doc_count
  )
  select
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  into
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  from final_counts;

  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', null,
    p_company_id, null, null, null,
    v_inserted_count, null, null, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered',  v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic',  v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  return jsonb_build_object(
    'msg', v_status_message,
    'time_ms', v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered',  v_total_docs,
    'top_per_tx', v_summary_top_per_tx,
    'top_per_doc', v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic',  v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

exception when others then
  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
  get stacked diagnostics
    v_err_state   = returned_sqlstate,
    v_err_msg     = message_text,
    v_err_detail  = pg_exception_detail,
    v_err_hint    = pg_exception_hint,
    v_err_context = pg_exception_context;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg,
    p_company_id, null, null, null,
    null, null, null, v_elapsed_ms,
    jsonb_build_object('sqlstate', v_err_state, 'detail', v_err_detail, 'hint', v_err_hint, 'context', v_err_context)
  );

  return jsonb_build_object(
    'msg','error','time_ms',v_elapsed_ms,
    'transactions_scanned',0,'documents_considered',0,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',v_err_msg,'sqlstate',v_err_state
  );
end;
$$;

alter function athena_reconcile_general_invoices_latest(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_invoices_latest(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_invoices_latest(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_invoices_latest(unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v1_9_o(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
declare
  v_status_message text := 'success';
  v_engine_version text := '1.9O';
  v_start_time timestamptz := clock_timestamp();
  v_elapsed_ms bigint := 0;

  v_config_amount_tolerance           numeric;
  v_config_date_window_days           numeric;
  v_config_min_confidence             numeric;
  v_config_allow_document_duplicates  boolean;
  v_config_log_activity               boolean;

  v_amount_tolerance_fraction numeric;
  v_date_window_seconds       bigint;
  v_date_full_weight_grace_days int := 30;

  v_edge_weight_small constant numeric := 0.10;
  v_gauss_k            constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small));

  v_cap_precise               int := 5;
  v_cap_broad                 int := 10;
  v_cap_window_only           int := 3;
  v_cap_iban_exact            int := 5;
  v_iban_exact_window_seconds bigint;
  v_max_transactions_to_scan  int := 20000;

  v_weights_json           jsonb := '{}'::jsonb;
  v_weights_total_possible numeric := 0;

  v_summary_top_per_tx   bigint := 0;
  v_summary_top_per_doc  bigint := 0;
  v_summary_passed_count bigint := 0;
  v_inserted_count       bigint := 0;
  v_candidates_count     bigint := 0;

  v_total_tx   bigint := 0;
  v_total_docs bigint := 0;

  v_seed_counts_total    jsonb := '{}'::jsonb;
  v_seed_counts_distinct jsonb := '{}'::jsonb;

  v_log_run_id        uuid := gen_random_uuid();
  v_log_run_unix_time bigint := extract(epoch from now())::bigint;

  v_directional_matching boolean := true;

  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;
begin
  select amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  into   v_config_amount_tolerance, v_config_date_window_days, v_config_min_confidence,
         v_config_allow_document_duplicates, v_config_log_activity
  from   public.reconciliations_config
  where  reconciliation_config_id = p_reconciliation_config_id;

  if not found then
    v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
    return jsonb_build_object('msg','reconciliations_config not found','time_ms',v_elapsed_ms,
                              'transactions_scanned',0,'documents_considered',0,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  end if;

  v_amount_tolerance_fraction := case
    when v_config_amount_tolerance >= 1 then v_config_amount_tolerance / 100.0
    else v_config_amount_tolerance
  end;

  v_date_window_seconds := ceil(coalesce(v_config_date_window_days, 1) * 86400)::bigint;
  v_iban_exact_window_seconds := least(v_date_window_seconds, 14 * 86400)::bigint;

  if v_config_min_confidence is null then v_config_min_confidence := 0; end if;
  if v_config_min_confidence <= 1 then v_config_min_confidence := v_config_min_confidence * 100.0; end if;

  with candidate_weights as (
    select lower(trim(w.weight_key)) as k_norm,
           (w.weight_value)::numeric as v_norm,
           w.company_id
    from public.reconciliations_weight_registry w
    where w.active
      and w.merchant_id is null
      and (
           (p_company_id is null and w.company_id is null)
        or (p_company_id is not null and (w.company_id = p_company_id or w.company_id is null))
      )
      and lower(trim(w.weight_key)) in (
        'amount_total','invoice_number','order_number','date','currency','merchant_id','customer_name'
      )
  ),
  picked as (
    select distinct on (k_norm) k_norm, v_norm
    from candidate_weights
    order by k_norm, (company_id is null)
  )
  select coalesce(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) into v_weights_json from picked;

  select coalesce((v_weights_json->>'amount_total')::numeric,0)
       + coalesce((v_weights_json->>'invoice_number')::numeric,0)
       + coalesce((v_weights_json->>'order_number')::numeric,0)
       + coalesce((v_weights_json->>'date')::numeric,0)
       + coalesce((v_weights_json->>'currency')::numeric,0)
       + coalesce((v_weights_json->>'merchant_id')::numeric,0)
       + coalesce((v_weights_json->>'customer_name')::numeric,0)
  into v_weights_total_possible;

  if v_weights_total_possible = 0 then
    v_weights_json := jsonb_build_object(
      'amount_total',1,'invoice_number',1,'order_number',1,'date',1,'currency',1,'merchant_id',1,'customer_name',1
    );
    v_weights_total_possible := 7;
  end if;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_start', 'function', 'ok', null,
    p_company_id, null, null, null,
    null, null, null, null,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', v_config_log_activity,
        'allow_doc_dups', v_config_allow_document_duplicates,
        'amount_tol_pct', v_amount_tolerance_fraction,
        'min_confidence', v_config_min_confidence,
        'date_window_days', v_config_date_window_days,
        'max_tx_seed', v_max_transactions_to_scan,
        'cap_precise', v_cap_precise,
        'cap_broad',   v_cap_broad,
        'cap_window_only', v_cap_window_only,
        'cap_iban_exact', v_cap_iban_exact,
        'iban_exact_window_days', round(v_iban_exact_window_seconds/86400.0,2),
        'directional_matching', v_directional_matching,
        'gauss_edge_weight', v_edge_weight_small
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  with
  document_scope as materialized (
    select
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      nullif(d.invoice_number, '') as invoice_number_raw,
      nullif(d.order_id, '')       as order_id_raw,
      nullif(public.norm_code(d.invoice_number), '') as invoice_number_norm,
      nullif(public.norm_code(d.order_id), '')       as order_id_norm,
      d.document_date_unix::bigint as document_date_unix,
      d.merchant_id,
      d.customer_name,
      nullif(public.norm_core_ns(coalesce(d.merchant_name, d.merchant_info::jsonb->>'name')), '') as doc_merchant_ns,
      nullif(public.norm_text_ns(
        concat_ws(' ',
          d.customer_name,
          d.customer_info::jsonb->>'name',
          d.customer_info::jsonb->>'contact_person',
          d.customer_info::jsonb->>'company_name',
          d.customer_info::jsonb->>'email'
        )
      ), '') as doc_customer_ns,
      nullif(public.norm_iban(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '')      as doc_iban_ns,
      nullif(public.norm_iban_core(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '') as doc_iban_core_ns,
      coalesce(
        d.customer_name,
        d.customer_info::jsonb->>'name',
        d.customer_info::jsonb->>'contact_person',
        d.customer_info::jsonb->>'company_name',
        d.customer_info::jsonb->>'email'
      ) as doc_customer_display,
      coalesce(nullif(d.dedup_serialized,''), d.document_id::text) as doc_dedup_key
    from public.documents_v2 d
    where (p_company_id is null or d.company_id = p_company_id)
      and d.reconciled is not true
  ),

  transactions_scope as materialized (
    select
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.invoice_nr,''),
            (select m[1]
             from regexp_matches(
               upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
               '(INV(?:OICE)?[ -_/]?\d{1,20}|[A-Z]{1,6}[-/]\d{2,20}|[0-9]{4}[-/][0-9]{2,20})',
               'g'
             ) as m
             limit 1)
          )
        ),
      '') as invoice_nr_norm,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.order_id,''),
            (select m[1]
             from regexp_matches(
               upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
               '(ORD(?:ER)?[ -_/]?\d{2,20}|PO[ -_/]?\d{2,20}|ORDER[ -_/]?\d{2,20})',
               'g'
             ) as m
             limit 1)
          )
        ),
      '') as order_id_norm,
      t.completed_date::bigint as completed_date,
      t.merchant_id,
      t.name_consumer,
      (nullif(t.credited,0) is not null or nullif(t.money_in,0)  is not null)  as tx_is_in,
      (nullif(t.debited,0)  is not null or nullif(t.money_out,0) is not null) as tx_is_out,
      nullif(public.norm_core_ns(
        coalesce(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
      ), '') as tx_merchant_ns,
      nullif(public.norm_text_ns(
        coalesce(
          t.name_consumer,
          t.account_holder_name,
          t.account_holder_name_counterparty
        )
      ), '') as tx_customer_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '')      as tx_iban_beneficiary_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '')             as tx_iban_counterparty_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '') as tx_iban_beneficiary_core_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '')        as tx_iban_counterparty_core_ns,
      coalesce(
        t.name_consumer,
        t.account_holder_name,
        t.account_holder_name_counterparty
      ) as tx_customer_display,
      case
        when t.debited  is not null and t.debited  > 0 then t.debited
        when t.credited is not null and t.credited > 0 then t.credited
        when t.amount   is not null                  then abs(t.amount)
        else null
      end as effective_amount
    from public.transactions t
    where (p_company_id is null or t.company_id = p_company_id::text)
      and t.reconciled is not true
      and (
           t.invoice_nr is not null
        or t.order_id   is not null
        or (t.currency is not null and t.merchant_id   is not null)
        or (t.currency is not null and (t.name_consumer is not null or t.account_holder_name is not null or t.account_holder_name_counterparty is not null))
        or (t.currency is not null and (t.amount is not null or t.debited is not null or t.credited is not null))
      )
    order by t.completed_date desc, t.transaction_id
    limit v_max_transactions_to_scan
  ),

  s1_invoice as (
    select t.transaction_id, d.document_id, 'invoice_number' as seed
    from transactions_scope t
    join document_scope d
      on d.company_id::text = t.company_id
     and d.invoice_number_norm is not null
     and t.invoice_nr_norm     is not null
     and d.invoice_number_norm = t.invoice_nr_norm
     and d.currency = t.currency
  ),

  s2_order as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'order_number' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and d.order_id_norm is not null
       and t.order_id_norm is not null
       and d.order_id_norm = t.order_id_norm
       and d.currency = t.currency
    ) s
    left join (select distinct transaction_id from s1_invoice) u using (transaction_id)
    where u.transaction_id is null
  ),

  s3_iban_exact_amount_out as (
    select t.transaction_id, d_sub.document_id, 'iban_exact_amount_out' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and t.debited is not null and t.debited > 0
        and d.total = t.debited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_sub on true
    where done.transaction_id is null
  ),
  s3_iban_exact_amount_in as (
    select t.transaction_id, d_sub.document_id, 'iban_exact_amount_in' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and t.credited is not null and t.credited > 0
        and d.total = t.credited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_sub on true
    where done.transaction_id is null
  ),
  s3_iban_exact_amount as (
    select * from s3_iban_exact_amount_out
    union all
    select * from s3_iban_exact_amount_in
  ),

  s4_merchant_id_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_id_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.merchant_id = t.merchant_id
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s5_merchant_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
       and (
         d.doc_merchant_ns = t.tx_merchant_ns
         or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
         or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
       )
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s6_customer_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'customer_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_in)
       and d.currency = t.currency
       and d.doc_customer_ns is not null and t.tx_customer_ns is not null
       and (
         d.doc_customer_ns = t.tx_customer_ns
         or position(d.doc_customer_ns in t.tx_customer_ns) > 0
         or position(t.tx_customer_ns in d.doc_customer_ns) > 0
       )
       and t.credited is not null and t.credited > 0
       and d.total = t.credited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s7_iban_plus_signal as (
    select t.transaction_id, d_sub.document_id, 'iban_plus_signal' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and (
             ( ((not v_directional_matching) or t.tx_is_out) and
               ( (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
                 or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns) ) )
          or ( ((not v_directional_matching) or t.tx_is_in ) and
               ( (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
                 or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns) ) )
        )
        and (
             (t.invoice_nr_norm is not null and d.invoice_number_norm is not null)
          or (t.order_id_norm   is not null and d.order_id_norm   is not null)
          or (t.tx_merchant_ns  is not null and d.doc_merchant_ns is not null)
          or (t.tx_customer_ns  is not null and d.doc_customer_ns is not null)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_precise
    ) d_sub on true
    where done.transaction_id is null
  ),

  s8_merchant_id_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_id_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.merchant_id = t.merchant_id
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction))
                        and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s9_merchant_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
        and (
          d.doc_merchant_ns = t.tx_merchant_ns
          or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
          or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
        )
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction))
                        and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s10_customer_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'customer_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and d.doc_customer_ns is not null and t.tx_customer_ns is not null
        and (
          d.doc_customer_ns = t.tx_customer_ns
          or position(d.doc_customer_ns in t.tx_customer_ns) > 0
          or position(t.tx_customer_ns in d.doc_customer_ns) > 0
        )
        and v_amount_tolerance_fraction > 0
        and t.credited is not null and t.credited > 0
        and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                        and (t.credited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.credited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s11_iban_tol as (
    select t.transaction_id, d_sub.document_id, 'iban_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and
            ( (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns) )
            and t.debited is not null and t.debited > 0
            and d.total between (t.debited  / (1 + v_amount_tolerance_fraction))
                            and (t.debited  / (1 - v_amount_tolerance_fraction)) )
          or
          ( ((not v_directional_matching) or t.tx_is_in) and
            ( (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns) )
            and t.credited is not null and t.credited > 0
            and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                            and (t.credited / (1 - v_amount_tolerance_fraction)) )
        )
      order by
        coalesce(abs(d.total - t.debited), abs(d.total - t.credited)),
        abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s12_window_amount_exact as (
    select t.transaction_id, d_sub.document_id, 'window_amount_exact' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and t.debited  is not null and t.debited  > 0 and d.total = t.debited )
          or
          ( ((not v_directional_matching) or t.tx_is_in ) and t.credited is not null and t.credited > 0 and d.total = t.credited)
        )
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s13_window_amount_tol as (
    select t.transaction_id, d_sub.document_id, 'window_amount_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and t.debited  is not null and t.debited  > 0
            and d.total between (t.debited  / (1 + v_amount_tolerance_fraction))
                            and (t.debited  / (1 - v_amount_tolerance_fraction)) )
          or
          ( ((not v_directional_matching) or t.tx_is_in ) and t.credited is not null and t.credited > 0
            and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                            and (t.credited / (1 - v_amount_tolerance_fraction)) )
        )
      order by
        coalesce(abs(d.total - t.debited), abs(d.total - t.credited)),
        abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s14_window_only as (
    select t.transaction_id, d_sub.document_id, 'window_only' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
      union all select distinct transaction_id from s13_window_amount_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_window_only
    ) d_sub on true
    where done.transaction_id is null
  ),

  seeds as (
    select * from s1_invoice
    union all select * from s2_order
    union all select * from s3_iban_exact_amount
    union all select * from s4_merchant_id_exact_amount
    union all select * from s5_merchant_ns_exact_amount
    union all select * from s6_customer_ns_exact_amount
    union all select * from s7_iban_plus_signal
    union all select * from s8_merchant_id_tol
    union all select * from s9_merchant_ns_tol
    union all select * from s10_customer_ns_tol
    union all select * from s11_iban_tol
    union all select * from s12_window_amount_exact
    union all select * from s13_window_amount_tol
    union all select * from s14_window_only
  ),

  seed_counts_total as (
    select seed, count(*) as ct from seeds group by seed
  ),
  seed_counts_distinct as (
    select seed, count(distinct (transaction_id, document_id)) as ct from seeds group by seed
  ),

  candidate_pairs as (
    select distinct transaction_id, document_id from seeds
  ),

  distinct_pairs as (
    select distinct transaction_id, document_id
    from candidate_pairs
  ),

  attributes as (
    select
      p.transaction_id,
      p.document_id,
      t.company_id::uuid as transaction_company_id,
      d.company_id       as document_company_id,
      t.amount as transaction_amount,
      t.debited as transaction_debited,
      t.credited as transaction_credited,
      d.total  as document_total,
      t.currency as transaction_currency,
      d.currency as document_currency,
      t.invoice_nr_norm as tx_invoice_norm,
      d.invoice_number_norm as doc_invoice_norm,
      t.order_id_norm  as tx_order_norm,
      d.order_id_norm  as doc_order_norm,
      d.invoice_number_raw as doc_invoice_raw,
      d.order_id_raw       as doc_order_raw,
      t.completed_date as transaction_date_unix,
      d.document_date_unix as document_date_unix,
      t.merchant_id as transaction_merchant_id,
      d.merchant_id as document_merchant_id,
      t.tx_customer_display as transaction_customer_name,
      d.doc_customer_display as document_customer_name,
      t.tx_is_in, t.tx_is_out,
      t.tx_merchant_ns, d.doc_merchant_ns,
      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_iban_beneficiary_ns,
      t.tx_iban_counterparty_ns,
      d.doc_iban_ns,
      t.tx_iban_beneficiary_core_ns,
      t.tx_iban_counterparty_core_ns,
      d.doc_iban_core_ns,
      coalesce(nullif(d.doc_dedup_key,''), d.document_id::text) as doc_dedup_key,
      t.effective_amount as transaction_effective_amount
    from distinct_pairs p
    join transactions_scope t on t.transaction_id = p.transaction_id
    join document_scope    d on d.document_id    = p.document_id
    where d.company_id::text = t.company_id
  ),

  evaluated as (
    select
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,
      coalesce((a.tx_invoice_norm is not null and a.doc_invoice_norm is not null
                and a.tx_invoice_norm = a.doc_invoice_norm), false) as invoice_number_match,
      coalesce((a.tx_order_norm   is not null and a.doc_order_norm   is not null
                and a.tx_order_norm = a.doc_order_norm), false)     as order_number_match,
      coalesce(a.transaction_currency  = a.document_currency, false) as currency_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_out and (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null
                  and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or (
                   (a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns)
                or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            ))
          else
            (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null
                  and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or (
                   (a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns)
                or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            )
        end
      ) as merchant_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_in and (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (
                    a.tx_customer_ns = a.doc_customer_ns
                 or position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or (
                   (a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            ))
          else
            (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (
                    a.tx_customer_ns = a.doc_customer_ns
                 or position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or (
                   (a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            )
        end
      ) as customer_match,
      coalesce(abs(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, false) as date_within_cap,
      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,
      (a.transaction_debited  is not null and a.transaction_debited  > 0 and a.document_total = a.transaction_debited)  as amount_exact_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and a.document_total = a.transaction_credited) as amount_exact_from_credited,
      (a.transaction_debited  is not null and a.transaction_debited  > 0 and abs(a.document_total - a.transaction_debited)  <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and abs(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_credited,
      a.tx_is_in, a.tx_is_out,
      a.tx_iban_beneficiary_ns,
      a.tx_iban_counterparty_ns,
      a.doc_iban_ns,
      a.tx_iban_beneficiary_core_ns,
      a.tx_iban_counterparty_core_ns,
      a.doc_iban_core_ns,
      a.tx_merchant_ns,
      a.doc_merchant_ns,
      a.tx_customer_ns,
      a.doc_customer_ns,
      a.transaction_merchant_id,
      a.document_merchant_id
    from attributes a
  ),

  with_amount_tolerance as (
    select
      e.*,
      case
        when e.tx_is_out and e.amount_exact_from_debited then true
        when e.tx_is_in  and e.amount_exact_from_credited then true
        when not v_directional_matching and (e.amount_exact_from_debited or e.amount_exact_from_credited) then true
        else false
      end as amount_exact_match,
      case
        when v_amount_tolerance_fraction > 0 then
          case
            when e.tx_is_out and e.amount_tolerance_from_debited then true
            when e.tx_is_in  and e.amount_tolerance_from_credited then true
            when not v_directional_matching and (e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) then true
            else false
          end
        else false
      end as amount_tolerance_match,
      (e.amount_exact_from_debited or e.amount_exact_from_credited or e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) as amount_direction_ok
    from evaluated e
  ),

  resolved_weights as (
    select
      coalesce((v_weights_json->>'amount_total')::numeric, 0)   as amount_weight,
      coalesce((v_weights_json->>'invoice_number')::numeric, 0) as invoice_weight,
      coalesce((v_weights_json->>'order_number')::numeric, 0)   as order_weight,
      coalesce((v_weights_json->>'date')::numeric, 0)           as date_weight,
      coalesce((v_weights_json->>'currency')::numeric, 0)       as currency_weight,
      coalesce((v_weights_json->>'merchant_id')::numeric, 0)    as merchant_weight,
      coalesce((v_weights_json->>'customer_name')::numeric, 0)  as customer_weight
  ),

  scored_components as (
    select
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,
      m.transaction_date_unix,
      m.document_date_unix,
      m.amount_direction_ok,
      m.tx_is_in, m.tx_is_out,
      m.tx_iban_beneficiary_ns,
      m.tx_iban_counterparty_ns,
      m.doc_iban_ns,
      m.tx_iban_beneficiary_core_ns,
      m.tx_iban_counterparty_core_ns,
      m.doc_iban_core_ns,
      m.tx_merchant_ns,
      m.doc_merchant_ns,
      m.tx_customer_ns,
      m.doc_customer_ns,
      m.transaction_merchant_id,
      m.document_merchant_id,
      coalesce(
        case
          when m.amount_exact_match then w.amount_weight
          when m.amount_tolerance_match then
            w.amount_weight * exp(
              -0.5 * power(
                (abs(m.transaction_effective_amount - m.document_total))
                / nullif((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0)
              , 2)
            )
          else 0
        end, 0
      )::numeric as amount_score,
      (m.amount_exact_match or m.amount_tolerance_match) as amount_match,
      m.invoice_number_match  as invoice_number_match,
      m.order_number_match    as order_number_match,
      case
        when m.transaction_date_unix is not null and m.document_date_unix is not null then
          w.date_weight * exp(
            -0.5 * power(
              greatest(0,
                (abs(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days
              ) / nullif( greatest(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k ), 0)
            , 2)
          )
        else 0
      end::numeric as date_score,
      m.date_within_cap as date_match,
      m.currency_match  as currency_match,
      m.merchant_match  as merchant_match,
      m.customer_match  as customer_match,
      w.amount_weight   as amount_weight,
      w.invoice_weight  as invoice_weight,
      w.order_weight    as order_weight,
      w.date_weight     as date_weight,
      w.currency_weight as currency_weight,
      w.merchant_weight as merchant_weight,
      w.customer_weight as customer_weight,
      (coalesce(m.invoice_number_match::int,0)  * w.invoice_weight)::numeric  as invoice_score,
      (coalesce(m.order_number_match::int,0)    * w.order_weight)::numeric    as order_score,
      (coalesce(m.currency_match::int,0)        * w.currency_weight)::numeric as currency_score,
      (coalesce(m.merchant_match::int,0)        * w.merchant_weight)::numeric as merchant_score,
      (coalesce(m.customer_match::int,0)        * w.customer_weight)::numeric as customer_score
    from with_amount_tolerance m
    cross join resolved_weights w
  ),

  scored_with_applicability as (
    select
      s.*,
      (
        case
          when v_directional_matching then
            ( s.tx_is_out and (
                 (s.doc_merchant_ns is not null and s.tx_merchant_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              or (s.document_merchant_id is not null and s.transaction_merchant_id is not null)
            ))
          else
            (
                 (s.doc_merchant_ns is not null and s.tx_merchant_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              or (s.document_merchant_id is not null and s.transaction_merchant_id is not null)
            )
        end
      ) as merchant_applicable,
      (
        case
          when v_directional_matching then
            ( s.tx_is_in and (
                 (s.doc_customer_ns is not null and s.tx_customer_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            ))
          else
            (
                 (s.doc_customer_ns is not null and s.tx_customer_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            )
        end
      ) as customer_applicable
    from scored_components s
  ),

  scored_with_confidence as (
    select
      s.*,
      (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      )::numeric as dynamic_total_weight,
      (
        s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
        + (case when s.merchant_applicable then s.merchant_score else 0 end)
        + (case when s.customer_applicable then s.customer_score else 0 end)
      )::numeric as total_numerator,
      (coalesce(s.amount_match::int,0)
     + coalesce(s.invoice_number_match::int,0)
     + coalesce(s.order_number_match::int,0)
     + coalesce(s.date_match::int,0)
     + coalesce(s.currency_match::int,0)
     + coalesce(s.merchant_match::int,0)
     + coalesce(s.customer_match::int,0)) as signals_matched_count,
      case when (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      ) > 0 then
        round(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
              + (case when s.merchant_applicable then s.merchant_score else 0 end)
              + (case when s.customer_applicable then s.customer_score else 0 end))
            / (s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
              + (case when s.merchant_applicable then s.merchant_weight else 0 end)
              + (case when s.customer_applicable then s.customer_weight else 0 end))) * 100.0, 4)
      else 0 end as confidence_percent
    from scored_with_applicability s
  ),

  low_signal as (select * from scored_with_confidence where signals_matched_count < 1),
  below_min_confidence as (
    select * from scored_with_confidence
    where signals_matched_count >= 1 and confidence_percent < v_config_min_confidence
  ),
  base_candidates as (
    select * from scored_with_confidence
    where signals_matched_count >= 1 and confidence_percent >= v_config_min_confidence
  ),

  ranked_per_transaction as (
    select
      b.*,
      row_number() over (
        partition by b.transaction_id
        order by b.confidence_percent desc, b.total_numerator desc, b.signals_matched_count desc, b.document_id
      ) as rownum_per_transaction
    from base_candidates b
  ),
  not_top_for_transaction as (select * from ranked_per_transaction where rownum_per_transaction > 1),
  top_per_transaction as (select * from ranked_per_transaction where rownum_per_transaction = 1),

  ranked_per_document as (
    select
      t.*,
      row_number() over (
        partition by t.doc_dedup_key
        order by
          t.confidence_percent desc,
          t.total_numerator desc,
          t.signals_matched_count desc,
          abs(t.transaction_date_unix - t.document_date_unix) asc,
          t.document_id
      ) as rownum_per_document
    from top_per_transaction t
  ),
  not_top_for_document as (select * from ranked_per_document where rownum_per_document > 1),
  top_per_document as (select * from ranked_per_document where rownum_per_document = 1),

  final_candidates as (
    select *
    from top_per_document
    order by confidence_percent desc, total_numerator desc, signals_matched_count desc, transaction_id, document_id
  ),

  allowed_for_insert as (
    select f.*
    from   final_candidates f
    where  not exists (select 1 from public.reconciliations r1 where r1.transaction_id = f.transaction_id)
       and not exists (select 1 from public.reconciliations r2 where r2.document_id    = f.document_id)
  ),

  inserted_reconciliations as (
    insert into public.reconciliations (
      transaction_id, document_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary, note
    )
    with ts_base as (
      select f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             coalesce(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint as base_unix
      from allowed_for_insert f
    ),
    enrich as (
      select
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match as invoice_match,
        d.order_number_match   as order_match,
        d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        d.amount_score, d.amount_weight,
        d.date_score,   d.date_weight,
        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm, a.tx_order_norm, a.doc_order_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw, a.doc_order_raw,
        a.tx_iban_beneficiary_ns, a.tx_iban_counterparty_ns, a.doc_iban_ns,
        a.tx_iban_beneficiary_core_ns, a.tx_iban_counterparty_core_ns, a.doc_iban_core_ns,
        a.tx_is_in, a.tx_is_out
      from ts_base t
      join final_candidates d on d.transaction_id = t.transaction_id and d.document_id = t.document_id
      join attributes a       on a.transaction_id = d.transaction_id and a.document_id = d.document_id
    ),
    notes as (
      select
        e.*,
        case
          when e.document_total is not null and e.document_total <> 0
          then round(abs(e.transaction_effective_amount - e.document_total) / nullif(e.document_total,0) * 100.0, 2)
          else null
        end as amt_diff_pct,
        case
          when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix)
          else null
        end as sec_diff,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 86400.0
          else null end as day_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 3600.0
          else null end as hour_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 60.0
          else null end as minute_gap,
        case when e.amount_weight > 0 then round((e.amount_score / e.amount_weight) * 100.0, 2) end as amount_ratio_pct,
        case when e.date_weight   > 0 then round((e.date_score   / e.date_weight)   * 100.0, 0)  end as date_ratio_pct
      from enrich e
    ),
    notes_text as (
      select
        n.*,
        case
          when n.date_weight > 0 and n.date_ratio_pct <= 99 then
            trim(both ' ' from concat_ws(' ',
              case
                when n.amount_weight > 0 and n.amount_score < n.amount_weight and n.amt_diff_pct is not null
                then concat('Amount scored ', coalesce(n.amount_ratio_pct::text,'0'),
                            '% because payment vs document differed by ', n.amt_diff_pct::text, '%.')
              end,
              case
                when n.day_gap is not null
                then concat('Date scored ', coalesce(n.date_ratio_pct::text,'0'),
                            '% because dates were ', round(n.day_gap::numeric, 2)::text, ' days apart.')
              end
            ))
          else null
        end as note_text
      from notes n
    )
    select
      e.transaction_id, e.document_id, e.document_company_id,
      coalesce(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      extract(year from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      extract(quarter from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) at time zone 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          select json_agg(s) from unnest(array[
            case when e.invoice_match  then 'invoice'  end,
            case when e.amount_match   then 'amount'   end,
            case when e.date_match     then 'date'     end,
            case when e.currency_match then 'currency' end,
            case when e.merchant_match then 'merchant' end,
            case when e.customer_match then 'customer' end,
            case when e.order_match    then 'order'    end
          ]) s where s is not null
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  coalesce(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_order',     e.tx_order_norm,
        'doc_order',    coalesce(e.doc_order_raw, e.doc_order_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_iban_beneficiary',        e.tx_iban_beneficiary_ns,
        'tx_iban_counterparty',       e.tx_iban_counterparty_ns,
        'doc_iban',                   e.doc_iban_ns,
        'tx_iban_beneficiary_core',   e.tx_iban_beneficiary_core_ns,
        'tx_iban_counterparty_core',  e.tx_iban_counterparty_core_ns,
        'doc_iban_core',              e.doc_iban_core_ns,
        'tx_is_in',    e.tx_is_in,
        'tx_is_out',   e.tx_is_out,
        'direction',   case when e.tx_is_in then 'incoming' when e.tx_is_out then 'outgoing' else 'unknown' end,
        'days_between_tx_and_doc',
          case
            when e.transaction_date_unix is not null and e.document_date_unix is not null
              then (abs(e.transaction_date_unix - e.document_date_unix) / 86400)::int
            else null
          end
      )::text,
      nullif(nt.note_text,'')
    from notes_text nt
    join enrich e using (transaction_id, document_id)
    returning reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores as (
    insert into public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score,  invoice_score,  order_score,  merchant_score,  amount_score,  currency_score,  customer_score
    )
    select
      r.reconciliation_id,
      d.document_company_id as company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      d.order_number_match,
      d.merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      d.order_weight,
      d.merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      d.order_score,
      d.merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    from inserted_reconciliations r
    join scored_with_confidence d
      on d.transaction_id = r.transaction_id
     and d.document_id    = r.document_id
    returning 1
  ),

  final_counts as (
    select
      (select count(*) from top_per_transaction)      as top_per_transaction_count,
      (select count(*) from top_per_document)         as top_per_document_count,
      (select count(*) from final_candidates)         as passed_confidence_count,
      (select count(*) from inserted_reconciliations) as inserted_count,
      (select count(*) from base_candidates)          as candidates_count,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_total)    as seed_counts_total_json,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_distinct) as seed_counts_distinct_json,
      (select count(*) from transactions_scope)       as total_tx_count,
      (select count(*) from document_scope)           as total_doc_count
  )
  select
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  into
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  from final_counts;

  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', null,
    p_company_id, null, null, null,
    v_inserted_count, null, null, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered',  v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic',  v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  return jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered',  v_total_docs,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic',  v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

exception when others then
  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
  get stacked diagnostics
    v_err_state   = returned_sqlstate,
    v_err_msg     = message_text,
    v_err_detail  = pg_exception_detail,
    v_err_hint    = pg_exception_hint,
    v_err_context = pg_exception_context;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg,
    p_company_id, null, null, null,
    null, null, null, v_elapsed_ms,
    jsonb_build_object(
      'sqlstate', v_err_state,
      'detail',   v_err_detail,
      'hint',     v_err_hint,
      'context',  v_err_context
    )
  );

  return jsonb_build_object(
    'msg','error','time_ms',v_elapsed_ms,
    'transactions_scanned',0,'documents_considered',0,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',v_err_msg,'sqlstate',v_err_state
  );
end;
$$;

alter function athena_reconcile_general_v1_9_o(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v1_9_o(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v1_9_o(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v1_9_o(unknown, unknown) to service_role;

create or replace function athena_reconcile_general_latest(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
declare
  v_status_message text := 'success';
  v_engine_version text := 'v1_9_P';
  v_start_time timestamptz := clock_timestamp();
  v_elapsed_ms bigint := 0;

  v_config_amount_tolerance numeric;
  v_config_date_window_days numeric;
  v_config_min_confidence numeric;
  v_config_allow_document_duplicates boolean;
  v_config_log_activity boolean;

  v_amount_tolerance_fraction numeric;
  v_date_window_seconds bigint;
  v_date_full_weight_grace_days int := 30;

  v_edge_weight_small constant numeric := 0.10;
  v_gauss_k constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small));

  v_cap_precise int := 5;
  v_cap_broad int := 10;
  v_cap_window_only int := 3;
  v_cap_iban_exact int := 5;
  v_iban_exact_window_seconds bigint;
  v_max_transactions_to_scan int := 20000;

  v_weights_json jsonb := '{}'::jsonb;
  v_weights_total_possible numeric := 0;

  v_summary_top_per_tx bigint := 0;
  v_summary_top_per_doc bigint := 0;
  v_summary_passed_count bigint := 0;
  v_inserted_count bigint := 0;
  v_candidates_count bigint := 0;

  v_total_tx bigint := 0;
  v_total_docs bigint := 0;

  v_seed_counts_total jsonb := '{}'::jsonb;
  v_seed_counts_distinct jsonb := '{}'::jsonb;

  v_log_run_id uuid := gen_random_uuid();
  v_log_run_unix_time bigint := extract(epoch from now())::bigint;

  v_directional_matching boolean := true;

  v_err_state text;
  v_err_msg text;
  v_err_detail text;
  v_err_hint text;
  v_err_context text;
begin
  select amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  into v_config_amount_tolerance, v_config_date_window_days, v_config_min_confidence, v_config_allow_document_duplicates, v_config_log_activity
  from public.reconciliations_config
  where reconciliation_config_id = p_reconciliation_config_id;

  if not found then
    v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
    return jsonb_build_object('msg','reconciliations_config not found','time_ms',v_elapsed_ms,'transactions_scanned',0,'documents_considered',0,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  end if;

  v_amount_tolerance_fraction := case when v_config_amount_tolerance >= 1 then v_config_amount_tolerance / 100.0 else v_config_amount_tolerance end;
  v_date_window_seconds := ceil(coalesce(v_config_date_window_days, 1) * 86400)::bigint;
  v_iban_exact_window_seconds := least(v_date_window_seconds, 14 * 86400)::bigint;

  if v_config_min_confidence is null then v_config_min_confidence := 0; end if;
  if v_config_min_confidence <= 1 then v_config_min_confidence := v_config_min_confidence * 100.0; end if;

  with candidate_weights as (
    select lower(trim(w.weight_key)) as k_norm, (w.weight_value)::numeric as v_norm, w.company_id
    from public.reconciliations_weight_registry w
    where w.active
      and w.merchant_id is null
      and ((p_company_id is null and w.company_id is null) or (p_company_id is not null and (w.company_id = p_company_id or w.company_id is null)))
      and lower(trim(w.weight_key)) in ('amount_total','invoice_number','order_number','date','currency','merchant_id','customer_name')
  ),
  picked as (
    select distinct on (k_norm) k_norm, v_norm
    from candidate_weights
    order by k_norm, (company_id is null)
  )
  select coalesce(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) into v_weights_json from picked;

  select coalesce((v_weights_json->>'amount_total')::numeric,0)
       + coalesce((v_weights_json->>'invoice_number')::numeric,0)
       + coalesce((v_weights_json->>'order_number')::numeric,0)
       + coalesce((v_weights_json->>'date')::numeric,0)
       + coalesce((v_weights_json->>'currency')::numeric,0)
       + coalesce((v_weights_json->>'merchant_id')::numeric,0)
       + coalesce((v_weights_json->>'customer_name')::numeric,0)
  into v_weights_total_possible;

  if v_weights_total_possible = 0 then
    v_weights_json := jsonb_build_object('amount_total',1,'invoice_number',1,'order_number',1,'date',1,'currency',1,'merchant_id',1,'customer_name',1);
    v_weights_total_possible := 7;
  end if;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message, company_id, transaction_id, document_id, reconciliation_id, rows_affected, confidence, step_ms, processing_ms, context
  ) values (
    v_log_run_id, v_engine_version, 'run_start', 'function', 'ok', null, p_company_id, null, null, null, null, null, null, null,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', v_config_log_activity,
        'allow_doc_dups', v_config_allow_document_duplicates,
        'amount_tol_pct', v_amount_tolerance_fraction,
        'min_confidence', v_config_min_confidence,
        'date_window_days', v_config_date_window_days,
        'max_tx_seed', v_max_transactions_to_scan,
        'cap_precise', v_cap_precise,
        'cap_broad', v_cap_broad,
        'cap_window_only', v_cap_window_only,
        'cap_iban_exact', v_cap_iban_exact,
        'iban_exact_window_days', round(v_iban_exact_window_seconds/86400.0,2),
        'directional_matching', v_directional_matching,
        'gauss_edge_weight', v_edge_weight_small
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  with
  document_scope as materialized (
    select
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      nullif(d.invoice_number, '') as invoice_number_raw,
      nullif(d.order_id, '') as order_id_raw,
      nullif(public.norm_code(d.invoice_number), '') as invoice_number_norm,
      nullif(public.norm_code(d.order_id), '') as order_id_norm,
      d.document_date_unix::bigint as document_date_unix,
      d.merchant_id,
      d.customer_name,
      nullif(public.norm_core_ns(coalesce(d.merchant_name, d.merchant_info::jsonb->>'name')), '') as doc_merchant_ns,
      nullif(public.norm_text_ns(
        concat_ws(' ',
          d.customer_name,
          d.customer_info::jsonb->>'name',
          d.customer_info::jsonb->>'contact_person',
          d.customer_info::jsonb->>'company_name',
          d.customer_info::jsonb->>'email'
        )
      ), '') as doc_customer_ns,
      nullif(public.norm_iban(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '') as doc_iban_ns,
      nullif(public.norm_iban_core(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '') as doc_iban_core_ns,
      coalesce(
        d.customer_name,
        d.customer_info::jsonb->>'name',
        d.customer_info::jsonb->>'contact_person',
        d.customer_info::jsonb->>'company_name',
        d.customer_info::jsonb->>'email'
      ) as doc_customer_display,
      coalesce(nullif(d.dedup_serialized,''), d.document_id::text) as doc_dedup_key
    from public.documents_v2 d
    where (p_company_id is null or d.company_id = p_company_id)
      and d.reconciled is not true
  ),

  transactions_scope as materialized (
    select
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.invoice_nr,''),
            (
              select match[1]
              from regexp_matches(
                upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
                '(?:\y(?:INV(?:OICE)?|FACTUUR(?:NR|NUMMER)?)\y\s*[-:#_/]*)?((?:\d{6}[-/]\d{2,20}|\d{4}[-/]\d{2,20}|[A-Z]{1,10}[-/]\d{2,20}|[A-Z0-9]{6,}))',
                'g'
              ) as match
              limit 1
            )
          )
        ),
      '') as invoice_nr_norm,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.order_id,''),
            (
              select match[1]
              from regexp_matches(
                upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
                '(?:\y(?:ORD(?:ER)?|PO|ORDER)\y\s*[-:#_/]*)?((?:\d{2,20}|[A-Z]{1,10}[-/]\d{2,20}))',
                'g'
              ) as match
              limit 1
            )
          )
        ),
      '') as order_id_norm,
      t.completed_date::bigint as completed_date,
      t.merchant_id,
      t.name_consumer,
      (nullif(t.credited,0) is not null or nullif(t.money_in,0) is not null) as tx_is_in,
      (nullif(t.debited,0) is not null or nullif(t.money_out,0) is not null) as tx_is_out,
      nullif(public.norm_core_ns(coalesce(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)), '') as tx_merchant_ns,
      nullif(public.norm_text_ns(coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty)), '') as tx_customer_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '') as tx_iban_beneficiary_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '') as tx_iban_counterparty_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '') as tx_iban_beneficiary_core_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '') as tx_iban_counterparty_core_ns,
      coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty) as tx_customer_display,
      case
        when t.debited is not null and t.debited > 0 then t.debited
        when t.credited is not null and t.credited > 0 then t.credited
        when t.amount is not null then abs(t.amount)
        else null
      end as effective_amount
    from public.transactions t
    where (p_company_id is null or t.company_id = p_company_id::text)
      and t.reconciled is not true
      and (
           t.invoice_nr is not null
        or t.order_id is not null
        or (t.currency is not null and t.merchant_id is not null)
        or (t.currency is not null and (t.name_consumer is not null or t.account_holder_name is not null or t.account_holder_name_counterparty is not null))
        or (t.currency is not null and (t.amount is not null or t.debited is not null or t.credited is not null))
      )
    order by t.completed_date desc, t.transaction_id
    limit v_max_transactions_to_scan
  ),

  s1_invoice as (
    select t.transaction_id, d.document_id, 'invoice_number' as seed
    from transactions_scope t
    join document_scope d
      on d.company_id::text = t.company_id
     and d.invoice_number_norm is not null
     and t.invoice_nr_norm is not null
     and d.invoice_number_norm = t.invoice_nr_norm
     and d.currency = t.currency
  ),

  s2_order as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'order_number' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and d.order_id_norm is not null
       and t.order_id_norm is not null
       and d.order_id_norm = t.order_id_norm
       and d.currency = t.currency
    ) s
    left join (select distinct transaction_id from s1_invoice) u using (transaction_id)
    where u.transaction_id is null
  ),

  s3_iban_exact_amount_out as (
    select t.transaction_id, d_out.document_id, 'iban_exact_amount_out' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_order) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and t.debited is not null and t.debited > 0
        and d.total = t.debited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_out on true
    where done.transaction_id is null
  ),

  s3_iban_exact_amount_in as (
    select t.transaction_id, d_in.document_id, 'iban_exact_amount_in' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_order) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and t.credited is not null and t.credited > 0
        and d.total = t.credited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_in on true
    where done.transaction_id is null
  ),

  s3_iban_exact_amount as (
    select * from s3_iban_exact_amount_out
    union all
    select * from s3_iban_exact_amount_in
  ),

  s4_merchant_id_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_id_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.merchant_id = t.merchant_id
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_order union all select distinct transaction_id from s3_iban_exact_amount) done using (transaction_id)
    where done.transaction_id is null
  ),

  s5_merchant_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
       and (d.doc_merchant_ns = t.tx_merchant_ns or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0 or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0)
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s6_customer_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'customer_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_in)
       and d.currency = t.currency
       and d.doc_customer_ns is not null and t.tx_customer_ns is not null
       and (d.doc_customer_ns = t.tx_customer_ns or position(d.doc_customer_ns in t.tx_customer_ns) > 0 or position(t.tx_customer_ns in d.doc_customer_ns) > 0)
       and t.credited is not null and t.credited > 0
       and d.total = t.credited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s7_iban_plus_signal as (
    select t.transaction_id, d_sub.document_id, 'iban_plus_signal' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and (
             (((not v_directional_matching) or t.tx_is_out) and ((d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns) or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns)))
          or (((not v_directional_matching) or t.tx_is_in)  and ((d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns) or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns)))
        )
        and (
             (t.invoice_nr_norm is not null and d.invoice_number_norm is not null)
          or (t.order_id_norm is not null and d.order_id_norm is not null)
          or (t.tx_merchant_ns is not null and d.doc_merchant_ns is not null)
          or (t.tx_customer_ns is not null and d.doc_customer_ns is not null)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_precise
    ) d_sub on true
    where done.transaction_id is null
  ),

  s8_merchant_id_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_id_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.merchant_id = t.merchant_id
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction)) and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s9_merchant_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
        and (d.doc_merchant_ns = t.tx_merchant_ns or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0 or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0)
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction)) and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s10_customer_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'customer_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and d.doc_customer_ns is not null and t.tx_customer_ns is not null
        and (d.doc_customer_ns = t.tx_customer_ns or position(d.doc_customer_ns in t.tx_customer_ns) > 0 or position(t.tx_customer_ns in d.doc_customer_ns) > 0)
        and v_amount_tolerance_fraction > 0
        and t.credited is not null and t.credited > 0
        and d.total between (t.credited / (1 + v_amount_tolerance_fraction)) and (t.credited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.credited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s11_iban_tol as (
    select t.transaction_id, d_sub.document_id, 'iban_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and (
          (
            ((not v_directional_matching) or t.tx_is_out)
            and (
              (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns)
            )
            and t.debited is not null and t.debited > 0
            and d.total between (t.debited / (1 + v_amount_tolerance_fraction)) and (t.debited / (1 - v_amount_tolerance_fraction))
          )
          or
          (
            ((not v_directional_matching) or t.tx_is_in)
            and (
              (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns)
            )
            and t.credited is not null and t.credited > 0
            and d.total between (t.credited / (1 + v_amount_tolerance_fraction)) and (t.credited / (1 - v_amount_tolerance_fraction))
          )
        )
      order by coalesce(abs(d.total - t.debited), abs(d.total - t.credited)), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s12_window_amount_exact as (
    select t.transaction_id, d_sub.document_id, 'window_amount_exact' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and d.total = coalesce(nullif(t.debited,0), nullif(t.credited,0), abs(t.amount))
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s13_window_amount_tol as (
    select t.transaction_id, d_sub.document_id, 'window_amount_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and d.total between (coalesce(nullif(t.debited,0), nullif(t.credited,0), abs(t.amount)) / (1 + v_amount_tolerance_fraction)) and (coalesce(nullif(t.debited,0), nullif(t.credited,0), abs(t.amount)) / (1 - v_amount_tolerance_fraction))
      order by abs(d.total - coalesce(nullif(t.debited,0), nullif(t.credited,0), abs(t.amount))), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s14_window_only as (
    select t.transaction_id, d_sub.document_id, 'window_only' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
      union all select distinct transaction_id from s13_window_amount_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_window_only
    ) d_sub on true
    where done.transaction_id is null
  ),

  seeds as (
    select * from s1_invoice
    union all select * from s2_order
    union all select * from s3_iban_exact_amount
    union all select * from s4_merchant_id_exact_amount
    union all select * from s5_merchant_ns_exact_amount
    union all select * from s6_customer_ns_exact_amount
    union all select * from s7_iban_plus_signal
    union all select * from s8_merchant_id_tol
    union all select * from s9_merchant_ns_tol
    union all select * from s10_customer_ns_tol
    union all select * from s11_iban_tol
    union all select * from s12_window_amount_exact
    union all select * from s13_window_amount_tol
    union all select * from s14_window_only
  ),

  seed_counts_total as (
    select seed, count(*) as ct from seeds group by seed
  ),
  seed_counts_distinct as (
    select seed, count(distinct (transaction_id, document_id)) as ct from seeds group by seed
  ),

  candidate_pairs as (
    select distinct transaction_id, document_id from seeds
  ),

  distinct_pairs as (
    select distinct transaction_id, document_id from candidate_pairs
  ),

  attributes as (
    select
      p.transaction_id,
      p.document_id,
      t.company_id::uuid as transaction_company_id,
      d.company_id as document_company_id,
      t.amount as transaction_amount,
      t.debited as transaction_debited,
      t.credited as transaction_credited,
      d.total as document_total,
      t.currency as transaction_currency,
      d.currency as document_currency,
      t.invoice_nr_norm as tx_invoice_norm,
      d.invoice_number_norm as doc_invoice_norm,
      t.order_id_norm as tx_order_norm,
      d.order_id_norm as doc_order_norm,
      d.invoice_number_raw as doc_invoice_raw,
      d.order_id_raw as doc_order_raw,
      t.completed_date as transaction_date_unix,
      d.document_date_unix as document_date_unix,
      t.merchant_id as transaction_merchant_id,
      d.merchant_id as document_merchant_id,
      t.tx_customer_display as transaction_customer_name,
      d.doc_customer_display as document_customer_name,
      t.tx_is_in, t.tx_is_out,
      t.tx_merchant_ns, d.doc_merchant_ns,
      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_iban_beneficiary_ns,
      t.tx_iban_counterparty_ns,
      d.doc_iban_ns,
      t.tx_iban_beneficiary_core_ns,
      t.tx_iban_counterparty_core_ns,
      d.doc_iban_core_ns,
      coalesce(nullif(d.doc_dedup_key,''), d.document_id::text) as doc_dedup_key,
      t.effective_amount as transaction_effective_amount
    from distinct_pairs p
    join transactions_scope t on t.transaction_id = p.transaction_id
    join document_scope d on d.document_id = p.document_id
    where d.company_id::text = t.company_id
  ),

  evaluated as (
    select
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,
      coalesce((a.tx_invoice_norm is not null and a.doc_invoice_norm is not null and a.tx_invoice_norm = a.doc_invoice_norm), false) as invoice_number_match,
      coalesce((a.tx_order_norm is not null and a.doc_order_norm is not null and a.tx_order_norm = a.doc_order_norm), false) as order_number_match,
      coalesce(a.transaction_currency = a.document_currency, false) as currency_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_out and (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (a.tx_merchant_ns = a.doc_merchant_ns or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or ((a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns) or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns))
            ))
          else
            (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (a.tx_merchant_ns = a.doc_merchant_ns or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or ((a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns) or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns))
            )
        end
      ) as merchant_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_in and (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (a.tx_customer_ns = a.doc_customer_ns or position(a.doc_customer_ns in a.tx_customer_ns) > 0 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or ((a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns) or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns))
            ))
          else
            (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (a.tx_customer_ns = a.doc_customer_ns or position(a.doc_customer_ns in a.tx_customer_ns) > 0 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or ((a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns) or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns))
            )
        end
      ) as customer_match,
      coalesce(abs(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, false) as date_within_cap,
      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,
      (a.transaction_debited is not null and a.transaction_debited > 0 and a.document_total = a.transaction_debited) as amount_exact_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and a.document_total = a.transaction_credited) as amount_exact_from_credited,
      (a.transaction_debited is not null and a.transaction_debited > 0 and abs(a.document_total - a.transaction_debited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and abs(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_credited,
      (a.transaction_effective_amount is not null and a.document_total = a.transaction_effective_amount) as amount_exact_from_effective,
      (a.transaction_effective_amount is not null and abs(a.document_total - a.transaction_effective_amount) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_effective,
      a.tx_is_in, a.tx_is_out,
      a.tx_iban_beneficiary_ns,
      a.tx_iban_counterparty_ns,
      a.doc_iban_ns,
      a.tx_iban_beneficiary_core_ns,
      a.tx_iban_counterparty_core_ns,
      a.doc_iban_core_ns,
      a.tx_merchant_ns,
      a.doc_merchant_ns,
      a.tx_customer_ns,
      a.doc_customer_ns,
      a.transaction_merchant_id,
      a.document_merchant_id
    from attributes a
  ),

  with_amount_tolerance as (
    select
      e.*,
      case
        when v_directional_matching and e.tx_is_out then e.amount_exact_from_debited
        when v_directional_matching and e.tx_is_in then e.amount_exact_from_credited
        else coalesce(e.amount_exact_from_debited,false) or coalesce(e.amount_exact_from_credited,false) or coalesce(e.amount_exact_from_effective,false)
      end as amount_exact_match,
      case
        when v_amount_tolerance_fraction > 0 then
          case
            when v_directional_matching and e.tx_is_out then e.amount_tolerance_from_debited
            when v_directional_matching and e.tx_is_in then e.amount_tolerance_from_credited
            else coalesce(e.amount_tolerance_from_debited,false) or coalesce(e.amount_tolerance_from_credited,false) or coalesce(e.amount_tolerance_from_effective,false)
          end
        else false
      end as amount_tolerance_match,
      (coalesce(e.amount_exact_from_debited,false) or coalesce(e.amount_exact_from_credited,false) or coalesce(e.amount_tolerance_from_debited,false) or coalesce(e.amount_tolerance_from_credited,false) or coalesce(e.amount_exact_from_effective,false) or coalesce(e.amount_tolerance_from_effective,false)) as amount_direction_ok
    from evaluated e
  ),

  resolved_weights as (
    select
      coalesce((v_weights_json->>'amount_total')::numeric, 0) as amount_weight,
      coalesce((v_weights_json->>'invoice_number')::numeric, 0) as invoice_weight,
      coalesce((v_weights_json->>'order_number')::numeric, 0) as order_weight,
      coalesce((v_weights_json->>'date')::numeric, 0) as date_weight,
      coalesce((v_weights_json->>'currency')::numeric, 0) as currency_weight,
      coalesce((v_weights_json->>'merchant_id')::numeric, 0) as merchant_weight,
      coalesce((v_weights_json->>'customer_name')::numeric, 0) as customer_weight
  ),

  scored_components as (
    select
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,
      m.transaction_date_unix,
      m.document_date_unix,
      m.amount_direction_ok,
      m.tx_is_in, m.tx_is_out,
      m.tx_iban_beneficiary_ns,
      m.tx_iban_counterparty_ns,
      m.doc_iban_ns,
      m.tx_iban_beneficiary_core_ns,
      m.tx_iban_counterparty_core_ns,
      m.doc_iban_core_ns,
      m.tx_merchant_ns,
      m.doc_merchant_ns,
      m.tx_customer_ns,
      m.doc_customer_ns,
      m.transaction_merchant_id,
      m.document_merchant_id,
      coalesce(
        case
          when m.amount_exact_match then w.amount_weight
          when m.amount_tolerance_match then w.amount_weight * exp(-0.5 * power((abs(m.transaction_effective_amount - m.document_total)) / nullif((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0), 2))
          else 0
        end, 0
      )::numeric as amount_score,
      (m.amount_exact_match or m.amount_tolerance_match) as amount_match,
      m.invoice_number_match as invoice_number_match,
      m.order_number_match as order_number_match,
      case
        when m.transaction_date_unix is not null and m.document_date_unix is not null then w.date_weight * exp(-0.5 * power(greatest(0,(abs(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days) / nullif(greatest(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k), 0), 2))
        else 0
      end::numeric as date_score,
      m.date_within_cap as date_match,
      m.currency_match as currency_match,
      m.merchant_match as merchant_match,
      m.customer_match as customer_match,
      w.amount_weight as amount_weight,
      w.invoice_weight as invoice_weight,
      w.order_weight as order_weight,
      w.date_weight as date_weight,
      w.currency_weight as currency_weight,
      w.merchant_weight as merchant_weight,
      w.customer_weight as customer_weight,
      (coalesce(m.invoice_number_match::int,0) * w.invoice_weight)::numeric as invoice_score,
      (coalesce(m.order_number_match::int,0) * w.order_weight)::numeric as order_score,
      (coalesce(m.currency_match::int,0) * w.currency_weight)::numeric as currency_score,
      (coalesce(m.merchant_match::int,0) * w.merchant_weight)::numeric as merchant_score,
      (coalesce(m.customer_match::int,0) * w.customer_weight)::numeric as customer_score
    from with_amount_tolerance m
    cross join resolved_weights w
  ),

  scored_with_applicability as (
    select
      s.*,
      (
        case
          when v_directional_matching then
            ( s.tx_is_out and ((s.doc_merchant_ns is not null and s.tx_merchant_ns is not null) or ((s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns) or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns)) or (s.document_merchant_id is not null and s.transaction_merchant_id is not null)) )
          else
            ( (s.doc_merchant_ns is not null and s.tx_merchant_ns is not null) or ((s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns) or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns)) or (s.document_merchant_id is not null and s.transaction_merchant_id is not null) )
        end
      ) as merchant_applicable,
      (
        case
          when v_directional_matching then
            ( s.tx_is_in and ((s.doc_customer_ns is not null and s.tx_customer_ns is not null) or ((s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns) or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns)) ) )
          else
            ( (s.doc_customer_ns is not null and s.tx_customer_ns is not null) or ((s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns) or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns)) )
        end
      ) as customer_applicable
    from scored_components s
  ),

  scored_with_confidence as (
    select
      s.*,
      (
        s.amount_weight + s.invoice_weight + s.order_weight
        + (case when s.transaction_date_unix is not null and s.document_date_unix is not null then s.date_weight else 0 end)
        + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      )::numeric as dynamic_total_weight,
      (
        s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
        + (case when s.merchant_applicable then s.merchant_score else 0 end)
        + (case when s.customer_applicable then s.customer_score else 0 end)
      )::numeric as total_numerator,
      (coalesce(s.amount_match::int,0) + coalesce(s.invoice_number_match::int,0) + coalesce(s.order_number_match::int,0) + coalesce(s.date_match::int,0) + coalesce(s.currency_match::int,0) + coalesce(s.merchant_match::int,0) + coalesce(s.customer_match::int,0)) as signals_matched_count,
      case when (
        s.amount_weight + s.invoice_weight + s.order_weight
        + (case when s.transaction_date_unix is not null and s.document_date_unix is not null then s.date_weight else 0 end)
        + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      ) > 0 then
        round(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + (case when s.merchant_applicable then s.merchant_score else 0 end) + (case when s.customer_applicable then s.customer_score else 0 end)) / (s.amount_weight + s.invoice_weight + s.order_weight + (case when s.transaction_date_unix is not null and s.document_date_unix is not null then s.date_weight else 0 end) + s.currency_weight + (case when s.merchant_applicable then s.merchant_weight else 0 end) + (case when s.customer_applicable then s.customer_weight else 0 end))) * 100.0, 4)
      else 0 end as confidence_percent
    from scored_with_applicability s
  ),

  low_signal as (select * from scored_with_confidence where signals_matched_count < 1),
  below_min_confidence as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent < v_config_min_confidence),
  base_candidates as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent >= v_config_min_confidence),

  ranked_per_transaction as (
    select b.*, row_number() over (partition by b.transaction_id order by b.confidence_percent desc, b.total_numerator desc, b.signals_matched_count desc, b.document_id) as rownum_per_transaction
    from base_candidates b
  ),
  not_top_for_transaction as (select * from ranked_per_transaction where rownum_per_transaction > 1),
  top_per_transaction as (select * from ranked_per_transaction where rownum_per_transaction = 1),

  ranked_per_document as (
    select t.*, row_number() over (partition by t.doc_dedup_key order by t.confidence_percent desc, t.total_numerator desc, t.signals_matched_count desc, abs(t.transaction_date_unix - t.document_date_unix) asc, t.document_id) as rownum_per_document
    from top_per_transaction t
  ),
  not_top_for_document as (select * from ranked_per_document where rownum_per_document > 1),
  top_per_document as (select * from ranked_per_document where rownum_per_document = 1),

  final_candidates as (
    select * from top_per_document
    order by confidence_percent desc, total_numerator desc, signals_matched_count desc, transaction_id, document_id
  ),

  allowed_for_insert as (
    select f.*
    from final_candidates f
    where not exists (select 1 from public.reconciliations r1 where r1.transaction_id = f.transaction_id)
      and not exists (select 1 from public.reconciliations r2 where r2.document_id = f.document_id)
  ),

  inserted_reconciliations as (
    insert into public.reconciliations (
      transaction_id, document_id, company_id, confidence, time, engine_version, fiscal_year, quarter, month, summary, note
    )
    with ts_base as (
      select f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent, coalesce(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint as base_unix
      from allowed_for_insert f
    ),
    enrich as (
      select
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match as invoice_match,
        d.order_number_match as order_match,
        d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        d.amount_score, d.amount_weight,
        d.date_score, d.date_weight,
        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm, a.tx_order_norm, a.doc_order_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw, a.doc_order_raw,
        a.tx_iban_beneficiary_ns, a.tx_iban_counterparty_ns, a.doc_iban_ns,
        a.tx_iban_beneficiary_core_ns, a.tx_iban_counterparty_core_ns, a.doc_iban_core_ns,
        a.tx_is_in, a.tx_is_out
      from ts_base t
      join final_candidates d on d.transaction_id = t.transaction_id and d.document_id = t.document_id
      join attributes a on a.transaction_id = d.transaction_id and a.document_id = d.document_id
    ),
    notes as (
      select
        e.*,
        case when e.document_total is not null and e.document_total <> 0 then round(abs(e.transaction_effective_amount - e.document_total) / nullif(e.document_total,0) * 100.0, 2) else null end as amt_diff_pct,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) else null end as sec_diff,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) / 86400.0 else null end as day_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) / 3600.0 else null end as hour_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) / 60.0 else null end as minute_gap,
        case when e.amount_weight > 0 then round((e.amount_score / e.amount_weight) * 100.0, 2) end as amount_ratio_pct,
        case when e.date_weight > 0 then round((e.date_score / e.date_weight) * 100.0, 0) end as date_ratio_pct
      from enrich e
    ),
    notes_text as (
      select
        n.*,
        case
          when n.date_weight > 0 and n.date_ratio_pct <= 99 then
            trim(both ' ' from concat_ws(' ',
              case when n.amount_weight > 0 and n.amount_score < n.amount_weight and n.amt_diff_pct is not null then concat('Amount scored ', coalesce(n.amount_ratio_pct::text,'0'), '% because payment vs document differed by ', n.amt_diff_pct::text, '%.') end,
              case when n.day_gap is not null then concat('Date scored ', coalesce(n.date_ratio_pct::text,'0'), '% because dates were ', round(n.day_gap::numeric, 2)::text, ' days apart.') end
            ))
          else null
        end as note_text
      from notes n
    )
    select
      e.transaction_id, e.document_id, e.document_company_id, coalesce(e.confidence_percent, 0)::numeric, v_log_run_unix_time, v_engine_version,
      extract(year from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      extract(quarter from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) at time zone 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (select json_agg(s) from unnest(array[
          case when e.invoice_match then 'invoice' end,
          case when e.amount_match then 'amount' end,
          case when e.date_match then 'date' end,
          case when e.currency_match then 'currency' end,
          case when e.merchant_match then 'merchant' end,
          case when e.customer_match then 'customer' end,
          case when e.order_match then 'order' end
        ]) s where s is not null),
        'tx_amount', e.transaction_effective_amount,
        'doc_total', e.document_total,
        'tx_invoice', e.tx_invoice_norm,
        'doc_invoice', coalesce(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_order', e.tx_order_norm,
        'doc_order', coalesce(e.doc_order_raw, e.doc_order_norm),
        'tx_currency', e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant', e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer', e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix', e.transaction_date_unix,
        'doc_unix', e.document_date_unix,
        'tx_iban_beneficiary', e.tx_iban_beneficiary_ns,
        'tx_iban_counterparty', e.tx_iban_counterparty_ns,
        'doc_iban', e.doc_iban_ns,
        'tx_iban_beneficiary_core', e.tx_iban_beneficiary_core_ns,
        'tx_iban_counterparty_core', e.tx_iban_counterparty_core_ns,
        'doc_iban_core', e.doc_iban_core_ns,
        'tx_is_in', e.tx_is_in,
        'tx_is_out', e.tx_is_out,
        'direction', case when e.tx_is_in then 'incoming' when e.tx_is_out then 'outgoing' else 'unknown' end,
        'days_between_tx_and_doc', case when e.transaction_date_unix is not null and e.document_date_unix is not null then (abs(e.transaction_date_unix - e.document_date_unix) / 86400)::int else null end
      )::text,
      nullif(nt.note_text,'')
    from notes_text nt
    join enrich e using (transaction_id, document_id)
    returning reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores as (
    insert into public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
    )
    select
      r.reconciliation_id,
      d.document_company_id as company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      d.order_number_match,
      d.merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      d.order_weight,
      d.merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      d.order_score,
      d.merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    from inserted_reconciliations r
    join scored_with_confidence d on d.transaction_id = r.transaction_id and d.document_id = r.document_id
    returning 1
  ),

  final_counts as (
    select
      (select count(*) from top_per_transaction) as top_per_transaction_count,
      (select count(*) from top_per_document) as top_per_document_count,
      (select count(*) from final_candidates) as passed_confidence_count,
      (select count(*) from inserted_reconciliations) as inserted_count,
      (select count(*) from base_candidates) as candidates_count,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_total) as seed_counts_total_json,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_distinct) as seed_counts_distinct_json,
      (select count(*) from transactions_scope) as total_tx_count,
      (select count(*) from document_scope) as total_doc_count
  )
  select
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  into
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  from final_counts;

  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message, company_id, transaction_id, document_id, reconciliation_id, rows_affected, confidence, step_ms, processing_ms, context
  ) values (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', null, p_company_id, null, null, null, v_inserted_count, null, null, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered', v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic', v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  return jsonb_build_object(
    'msg', v_status_message,
    'time_ms', v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered', v_total_docs,
    'top_per_tx', v_summary_top_per_tx,
    'top_per_doc', v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic', v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

exception when others then
  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
  get stacked diagnostics v_err_state = returned_sqlstate, v_err_msg = message_text, v_err_detail = pg_exception_detail, v_err_hint = pg_exception_hint, v_err_context = pg_exception_context;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message, company_id, transaction_id, document_id, reconciliation_id, rows_affected, confidence, step_ms, processing_ms, context
  ) values (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg, p_company_id, null, null, null, null, null, null, v_elapsed_ms,
    jsonb_build_object('sqlstate', v_err_state, 'detail', v_err_detail, 'hint', v_err_hint, 'context', v_err_context)
  );

  return jsonb_build_object('msg','error','time_ms',v_elapsed_ms,'transactions_scanned',0,'documents_considered',0,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,'error',v_err_msg,'sqlstate',v_err_state);
end;
$$;

alter function athena_reconcile_general_latest(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_latest(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_latest(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_latest(unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v1_9_p(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
declare
  v_status_message text := 'success';
  v_engine_version text := '1.9P';
  v_start_time timestamptz := clock_timestamp();
  v_elapsed_ms bigint := 0;

  v_config_amount_tolerance           numeric;
  v_config_date_window_days           numeric;
  v_config_min_confidence             numeric;
  v_config_allow_document_duplicates  boolean;
  v_config_log_activity               boolean;

  v_amount_tolerance_fraction numeric;
  v_date_window_seconds       bigint;
  v_date_full_weight_grace_days int := 30;

  v_edge_weight_small constant numeric := 0.10;
  v_gauss_k            constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small));

  v_cap_precise               int := 5;
  v_cap_broad                 int := 10;
  v_cap_window_only           int := 3;
  v_cap_iban_exact            int := 5;
  v_iban_exact_window_seconds bigint;
  v_max_transactions_to_scan  int := 20000;

  v_weights_json           jsonb := '{}'::jsonb;
  v_weights_total_possible numeric := 0;

  v_summary_top_per_tx   bigint := 0;
  v_summary_top_per_doc  bigint := 0;
  v_summary_passed_count bigint := 0;
  v_inserted_count       bigint := 0;
  v_candidates_count     bigint := 0;

  v_total_tx   bigint := 0;
  v_total_docs bigint := 0;

  v_seed_counts_total    jsonb := '{}'::jsonb;
  v_seed_counts_distinct jsonb := '{}'::jsonb;

  v_log_run_id        uuid := gen_random_uuid();
  v_log_run_unix_time bigint := extract(epoch from now())::bigint;

  v_directional_matching boolean := true;

  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;
begin
  select amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  into   v_config_amount_tolerance, v_config_date_window_days, v_config_min_confidence,
         v_config_allow_document_duplicates, v_config_log_activity
  from   public.reconciliations_config
  where  reconciliation_config_id = p_reconciliation_config_id;

  if not found then
    v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
    return jsonb_build_object('msg','reconciliations_config not found','time_ms',v_elapsed_ms,
                              'transactions_scanned',0,'documents_considered',0,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  end if;

  v_amount_tolerance_fraction := case
    when v_config_amount_tolerance >= 1 then v_config_amount_tolerance / 100.0
    else v_config_amount_tolerance
  end;

  v_date_window_seconds := ceil(coalesce(v_config_date_window_days, 1) * 86400)::bigint;
  v_iban_exact_window_seconds := least(v_date_window_seconds, 14 * 86400)::bigint;

  if v_config_min_confidence is null then v_config_min_confidence := 0; end if;
  if v_config_min_confidence <= 1 then v_config_min_confidence := v_config_min_confidence * 100.0; end if;

  with candidate_weights as (
    select lower(trim(w.weight_key)) as k_norm,
           (w.weight_value)::numeric as v_norm,
           w.company_id
    from public.reconciliations_weight_registry w
    where w.active
      and w.merchant_id is null
      and (
           (p_company_id is null and w.company_id is null)
        or (p_company_id is not null and (w.company_id = p_company_id or w.company_id is null))
      )
      and lower(trim(w.weight_key)) in (
        'amount_total','invoice_number','order_number','date','currency','merchant_id','customer_name'
      )
  ),
  picked as (
    select distinct on (k_norm) k_norm, v_norm
    from candidate_weights
    order by k_norm, (company_id is null)
  )
  select coalesce(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) into v_weights_json from picked;

  select coalesce((v_weights_json->>'amount_total')::numeric,0)
       + coalesce((v_weights_json->>'invoice_number')::numeric,0)
       + coalesce((v_weights_json->>'order_number')::numeric,0)
       + coalesce((v_weights_json->>'date')::numeric,0)
       + coalesce((v_weights_json->>'currency')::numeric,0)
       + coalesce((v_weights_json->>'merchant_id')::numeric,0)
       + coalesce((v_weights_json->>'customer_name')::numeric,0)
  into v_weights_total_possible;

  if v_weights_total_possible = 0 then
    v_weights_json := jsonb_build_object(
      'amount_total',1,'invoice_number',1,'order_number',1,'date',1,'currency',1,'merchant_id',1,'customer_name',1
    );
    v_weights_total_possible := 7;
  end if;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_start', 'function', 'ok', null,
    p_company_id, null, null, null,
    null, null, null, null,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', v_config_log_activity,
        'allow_doc_dups', v_config_allow_document_duplicates,
        'amount_tol_pct', v_amount_tolerance_fraction,
        'min_confidence', v_config_min_confidence,
        'date_window_days', v_config_date_window_days,
        'max_tx_seed', v_max_transactions_to_scan,
        'cap_precise', v_cap_precise,
        'cap_broad',   v_cap_broad,
        'cap_window_only', v_cap_window_only,
        'cap_iban_exact', v_cap_iban_exact,
        'iban_exact_window_days', round(v_iban_exact_window_seconds/86400.0,2),
        'directional_matching', v_directional_matching,
        'gauss_edge_weight', v_edge_weight_small
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  with
  document_scope as materialized (
    select
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      nullif(d.invoice_number, '') as invoice_number_raw,
      nullif(d.order_id, '')       as order_id_raw,
      nullif(public.norm_code(d.invoice_number), '') as invoice_number_norm,
      nullif(public.norm_code(d.order_id), '')       as order_id_norm,
      d.document_date_unix::bigint as document_date_unix,
      d.merchant_id,
      d.customer_name,
      nullif(public.norm_core_ns(coalesce(d.merchant_name, d.merchant_info::jsonb->>'name')), '') as doc_merchant_ns,
      nullif(public.norm_text_ns(
        concat_ws(' ',
          d.customer_name,
          d.customer_info::jsonb->>'name',
          d.customer_info::jsonb->>'contact_person',
          d.customer_info::jsonb->>'company_name',
          d.customer_info::jsonb->>'email'
        )
      ), '') as doc_customer_ns,
      nullif(public.norm_iban(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '')      as doc_iban_ns,
      nullif(public.norm_iban_core(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '') as doc_iban_core_ns,
      coalesce(
        d.customer_name,
        d.customer_info::jsonb->>'name',
        d.customer_info::jsonb->>'contact_person',
        d.customer_info::jsonb->>'company_name',
        d.customer_info::jsonb->>'email'
      ) as doc_customer_display,
      coalesce(nullif(d.dedup_serialized,''), d.document_id::text) as doc_dedup_key
    from public.documents_v2 d
    where (p_company_id is null or d.company_id = p_company_id)
      and d.reconciled is not true
  ),

  transactions_scope as materialized (
    select
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.invoice_nr,''),
            (select m[1]
             from regexp_matches(
               upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
               '(INV(?:OICE)?[ -_/]?\d{1,20}|FACTUUR[ -_/]?\d{1,20}|\d{6}[-/]\d{2,20}|[A-Z]{1,10}[-/]\d{2,20}|\d{4}[-/]\d{2,20})',
               'g'
             ) as m
             limit 1)
          )
        ),
      '') as invoice_nr_norm,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.order_id,''),
            (select m[1]
             from regexp_matches(
               upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
               '(ORD(?:ER)?[ -_/]?\d{2,20}|PO[ -_/]?\d{2,20}|ORDER[ -_/]?\d{2,20})',
               'g'
             ) as m
             limit 1)
          )
        ),
      '') as order_id_norm,
      t.completed_date::bigint as completed_date,
      t.merchant_id,
      t.name_consumer,
      (nullif(t.credited,0) is not null or nullif(t.money_in,0)  is not null)  as tx_is_in,
      (nullif(t.debited,0)  is not null or nullif(t.money_out,0) is not null) as tx_is_out,
      nullif(public.norm_core_ns(
        coalesce(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
      ), '') as tx_merchant_ns,
      nullif(public.norm_text_ns(
        coalesce(
          t.name_consumer,
          t.account_holder_name,
          t.account_holder_name_counterparty
        )
      ), '') as tx_customer_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '')      as tx_iban_beneficiary_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '')             as tx_iban_counterparty_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '') as tx_iban_beneficiary_core_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '')        as tx_iban_counterparty_core_ns,
      coalesce(
        t.name_consumer,
        t.account_holder_name,
        t.account_holder_name_counterparty
      ) as tx_customer_display,
      case
        when t.debited  is not null and t.debited  > 0 then t.debited
        when t.credited is not null and t.credited > 0 then t.credited
        when t.amount   is not null                  then abs(t.amount)
        else null
      end as effective_amount
    from public.transactions t
    where (p_company_id is null or t.company_id = p_company_id::text)
      and t.reconciled is not true
      and (
           t.invoice_nr is not null
        or t.order_id   is not null
        or (t.currency is not null and t.merchant_id   is not null)
        or (t.currency is not null and (t.name_consumer is not null or t.account_holder_name is not null or t.account_holder_name_counterparty is not null))
        or (t.currency is not null and (t.amount is not null or t.debited is not null or t.credited is not null))
      )
    order by t.completed_date desc, t.transaction_id
    limit v_max_transactions_to_scan
  ),

  s1_invoice as (
    select t.transaction_id, d.document_id, 'invoice_number' as seed
    from transactions_scope t
    join document_scope d
      on d.company_id::text = t.company_id
     and d.invoice_number_norm is not null
     and t.invoice_nr_norm     is not null
     and d.invoice_number_norm = t.invoice_nr_norm
     and d.currency = t.currency
  ),

  s2_order as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'order_number' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and d.order_id_norm is not null
       and t.order_id_norm is not null
       and d.order_id_norm = t.order_id_norm
       and d.currency = t.currency
    ) s
    left join (select distinct transaction_id from s1_invoice) u using (transaction_id)
    where u.transaction_id is null
  ),

  s3_iban_exact_amount_out as (
    select t.transaction_id, d_sub.document_id, 'iban_exact_amount_out' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and t.debited is not null and t.debited > 0
        and d.total = t.debited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_sub on true
    where done.transaction_id is null
  ),
  s3_iban_exact_amount_in as (
    select t.transaction_id, d_sub.document_id, 'iban_exact_amount_in' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and t.credited is not null and t.credited > 0
        and d.total = t.credited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_sub on true
    where done.transaction_id is null
  ),
  s3_iban_exact_amount as (
    select * from s3_iban_exact_amount_out
    union all
    select * from s3_iban_exact_amount_in
  ),

  s4_merchant_id_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_id_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.merchant_id = t.merchant_id
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s5_merchant_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
       and (
         d.doc_merchant_ns = t.tx_merchant_ns
         or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
         or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
       )
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s6_customer_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'customer_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_in)
       and d.currency = t.currency
       and d.doc_customer_ns is not null and t.tx_customer_ns is not null
       and (
         d.doc_customer_ns = t.tx_customer_ns
         or position(d.doc_customer_ns in t.tx_customer_ns) > 0
         or position(t.tx_customer_ns in d.doc_customer_ns) > 0
       )
       and t.credited is not null and t.credited > 0
       and d.total = t.credited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s7_iban_plus_signal as (
    select t.transaction_id, d_sub.document_id, 'iban_plus_signal' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and (
             ( ((not v_directional_matching) or t.tx_is_out) and
               ( (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
                 or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns) ) )
          or ( ((not v_directional_matching) or t.tx_is_in ) and
               ( (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
                 or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns) ) )
        )
        and (
             (t.invoice_nr_norm is not null and d.invoice_number_norm is not null)
          or (t.order_id_norm   is not null and d.order_id_norm   is not null)
          or (t.tx_merchant_ns  is not null and d.doc_merchant_ns is not null)
          or (t.tx_customer_ns  is not null and d.doc_customer_ns is not null)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_precise
    ) d_sub on true
    where done.transaction_id is null
  ),

  s8_merchant_id_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_id_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.merchant_id = t.merchant_id
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction))
                        and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s9_merchant_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
        and (
          d.doc_merchant_ns = t.tx_merchant_ns
          or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
          or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
        )
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction))
                        and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s10_customer_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'customer_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and d.doc_customer_ns is not null and t.tx_customer_ns is not null
        and (
          d.doc_customer_ns = t.tx_customer_ns
          or position(d.doc_customer_ns in t.tx_customer_ns) > 0
          or position(t.tx_customer_ns in d.doc_customer_ns) > 0
        )
        and v_amount_tolerance_fraction > 0
        and t.credited is not null and t.credited > 0
        and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                        and (t.credited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.credited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s11_iban_tol as (
    select t.transaction_id, d_sub.document_id, 'iban_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and
            ( (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns) )
            and t.debited is not null and t.debited > 0
            and d.total between (t.debited  / (1 + v_amount_tolerance_fraction))
                            and (t.debited  / (1 - v_amount_tolerance_fraction)) )
          or
          ( ((not v_directional_matching) or t.tx_is_in) and
            ( (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns) )
            and t.credited is not null and t.credited > 0
            and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                            and (t.credited / (1 - v_amount_tolerance_fraction)) )
        )
      order by
        coalesce(abs(d.total - t.debited), abs(d.total - t.credited)),
        abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s12_window_amount_exact as (
    select t.transaction_id, d_sub.document_id, 'window_amount_exact' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and t.debited  is not null and t.debited  > 0 and d.total = t.debited )
          or
          ( ((not v_directional_matching) or t.tx_is_in ) and t.credited is not null and t.credited > 0 and d.total = t.credited)
        )
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s13_window_amount_tol as (
    select t.transaction_id, d_sub.document_id, 'window_amount_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and t.debited  is not null and t.debited  > 0
            and d.total between (t.debited  / (1 + v_amount_tolerance_fraction))
                            and (t.debited  / (1 - v_amount_tolerance_fraction)) )
          or
          ( ((not v_directional_matching) or t.tx_is_in ) and t.credited is not null and t.credited > 0
            and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                            and (t.credited / (1 - v_amount_tolerance_fraction)) )
        )
      order by
        coalesce(abs(d.total - t.debited), abs(d.total - t.credited)),
        abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s14_window_only as (
    select t.transaction_id, d_sub.document_id, 'window_only' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
      union all select distinct transaction_id from s13_window_amount_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_window_only
    ) d_sub on true
    where done.transaction_id is null
  ),

  seeds as (
    select * from s1_invoice
    union all select * from s2_order
    union all select * from s3_iban_exact_amount
    union all select * from s4_merchant_id_exact_amount
    union all select * from s5_merchant_ns_exact_amount
    union all select * from s6_customer_ns_exact_amount
    union all select * from s7_iban_plus_signal
    union all select * from s8_merchant_id_tol
    union all select * from s9_merchant_ns_tol
    union all select * from s10_customer_ns_tol
    union all select * from s11_iban_tol
    union all select * from s12_window_amount_exact
    union all select * from s13_window_amount_tol
    union all select * from s14_window_only
  ),

  seed_counts_total as (
    select seed, count(*) as ct from seeds group by seed
  ),
  seed_counts_distinct as (
    select seed, count(distinct (transaction_id, document_id)) as ct from seeds group by seed
  ),

  candidate_pairs as (
    select distinct transaction_id, document_id from seeds
  ),

  distinct_pairs as (
    select distinct transaction_id, document_id
    from candidate_pairs
  ),

  attributes as (
    select
      p.transaction_id,
      p.document_id,
      t.company_id::uuid as transaction_company_id,
      d.company_id       as document_company_id,
      t.amount as transaction_amount,
      t.debited as transaction_debited,
      t.credited as transaction_credited,
      d.total  as document_total,
      t.currency as transaction_currency,
      d.currency as document_currency,
      t.invoice_nr_norm as tx_invoice_norm,
      d.invoice_number_norm as doc_invoice_norm,
      t.order_id_norm  as tx_order_norm,
      d.order_id_norm  as doc_order_norm,
      d.invoice_number_raw as doc_invoice_raw,
      d.order_id_raw       as doc_order_raw,
      t.completed_date as transaction_date_unix,
      d.document_date_unix as document_date_unix,
      t.merchant_id as transaction_merchant_id,
      d.merchant_id as document_merchant_id,
      t.tx_customer_display as transaction_customer_name,
      d.doc_customer_display as document_customer_name,
      t.tx_is_in, t.tx_is_out,
      t.tx_merchant_ns, d.doc_merchant_ns,
      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_iban_beneficiary_ns,
      t.tx_iban_counterparty_ns,
      d.doc_iban_ns,
      t.tx_iban_beneficiary_core_ns,
      t.tx_iban_counterparty_core_ns,
      d.doc_iban_core_ns,
      coalesce(nullif(d.doc_dedup_key,''), d.document_id::text) as doc_dedup_key,
      t.effective_amount as transaction_effective_amount
    from distinct_pairs p
    join transactions_scope t on t.transaction_id = p.transaction_id
    join document_scope    d on d.document_id    = p.document_id
    where d.company_id::text = t.company_id
  ),

  evaluated as (
    select
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,
      coalesce((a.tx_invoice_norm is not null and a.doc_invoice_norm is not null
                and a.tx_invoice_norm = a.doc_invoice_norm), false) as invoice_number_match,
      coalesce((a.tx_order_norm   is not null and a.doc_order_norm   is not null
                and a.tx_order_norm = a.doc_order_norm), false)     as order_number_match,
      coalesce(a.transaction_currency  = a.document_currency, false) as currency_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_out and (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null
                  and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or (
                   (a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns)
                or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            ))
          else
            (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null
                  and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or (
                   (a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns)
                or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            )
        end
      ) as merchant_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_in and (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (
                    a.tx_customer_ns = a.doc_customer_ns
                 or position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or (
                   (a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            ))
          else
            (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (
                    a.tx_customer_ns = a.doc_customer_ns
                 or position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or (
                   (a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            )
        end
      ) as customer_match,
      coalesce(abs(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, false) as date_within_cap,
      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,
      (a.transaction_debited  is not null and a.transaction_debited  > 0 and a.document_total = a.transaction_debited)  as amount_exact_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and a.document_total = a.transaction_credited) as amount_exact_from_credited,
      (a.transaction_debited  is not null and a.transaction_debited  > 0 and abs(a.document_total - a.transaction_debited)  <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and abs(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_credited,
      a.tx_is_in, a.tx_is_out,
      a.tx_iban_beneficiary_ns,
      a.tx_iban_counterparty_ns,
      a.doc_iban_ns,
      a.tx_iban_beneficiary_core_ns,
      a.tx_iban_counterparty_core_ns,
      a.doc_iban_core_ns,
      a.tx_merchant_ns,
      a.doc_merchant_ns,
      a.tx_customer_ns,
      a.doc_customer_ns,
      a.transaction_merchant_id,
      a.document_merchant_id
    from attributes a
  ),

  with_amount_tolerance as (
    select
      e.*,
      case
        when e.tx_is_out and e.amount_exact_from_debited then true
        when e.tx_is_in  and e.amount_exact_from_credited then true
        when not v_directional_matching and (e.amount_exact_from_debited or e.amount_exact_from_credited) then true
        else false
      end as amount_exact_match,
      case
        when v_amount_tolerance_fraction > 0 then
          case
            when e.tx_is_out and e.amount_tolerance_from_debited then true
            when e.tx_is_in  and e.amount_tolerance_from_credited then true
            when not v_directional_matching and (e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) then true
            else false
          end
        else false
      end as amount_tolerance_match,
      (e.amount_exact_from_debited or e.amount_exact_from_credited or e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) as amount_direction_ok
    from evaluated e
  ),

  resolved_weights as (
    select
      coalesce((v_weights_json->>'amount_total')::numeric, 0)   as amount_weight,
      coalesce((v_weights_json->>'invoice_number')::numeric, 0) as invoice_weight,
      coalesce((v_weights_json->>'order_number')::numeric, 0)   as order_weight,
      coalesce((v_weights_json->>'date')::numeric, 0)           as date_weight,
      coalesce((v_weights_json->>'currency')::numeric, 0)       as currency_weight,
      coalesce((v_weights_json->>'merchant_id')::numeric, 0)    as merchant_weight,
      coalesce((v_weights_json->>'customer_name')::numeric, 0)  as customer_weight
  ),

  scored_components as (
    select
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,
      m.transaction_date_unix,
      m.document_date_unix,
      m.amount_direction_ok,
      m.tx_is_in, m.tx_is_out,
      m.tx_iban_beneficiary_ns,
      m.tx_iban_counterparty_ns,
      m.doc_iban_ns,
      m.tx_iban_beneficiary_core_ns,
      m.tx_iban_counterparty_core_ns,
      m.doc_iban_core_ns,
      m.tx_merchant_ns,
      m.doc_merchant_ns,
      m.tx_customer_ns,
      m.doc_customer_ns,
      m.transaction_merchant_id,
      m.document_merchant_id,
      coalesce(
        case
          when m.amount_exact_match then w.amount_weight
          when m.amount_tolerance_match then
            w.amount_weight * exp(
              -0.5 * power(
                (abs(m.transaction_effective_amount - m.document_total))
                / nullif((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0)
              , 2)
            )
          else 0
        end, 0
      )::numeric as amount_score,
      (m.amount_exact_match or m.amount_tolerance_match) as amount_match,
      m.invoice_number_match  as invoice_number_match,
      m.order_number_match    as order_number_match,
      case
        when m.transaction_date_unix is not null and m.document_date_unix is not null then
          w.date_weight * exp(
            -0.5 * power(
              greatest(0,
                (abs(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days
              ) / nullif( greatest(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k ), 0)
            , 2)
          )
        else 0
      end::numeric as date_score,
      m.date_within_cap as date_match,
      m.currency_match  as currency_match,
      m.merchant_match  as merchant_match,
      m.customer_match  as customer_match,
      w.amount_weight   as amount_weight,
      w.invoice_weight  as invoice_weight,
      w.order_weight    as order_weight,
      w.date_weight     as date_weight,
      w.currency_weight as currency_weight,
      w.merchant_weight as merchant_weight,
      w.customer_weight as customer_weight,
      (coalesce(m.invoice_number_match::int,0)  * w.invoice_weight)::numeric  as invoice_score,
      (coalesce(m.order_number_match::int,0)    * w.order_weight)::numeric    as order_score,
      (coalesce(m.currency_match::int,0)        * w.currency_weight)::numeric as currency_score,
      (coalesce(m.merchant_match::int,0)        * w.merchant_weight)::numeric as merchant_score,
      (coalesce(m.customer_match::int,0)        * w.customer_weight)::numeric as customer_score
    from with_amount_tolerance m
    cross join resolved_weights w
  ),

  scored_with_applicability as (
    select
      s.*,
      (
        case
          when v_directional_matching then
            ( s.tx_is_out and (
                 (s.doc_merchant_ns is not null and s.tx_merchant_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              or (s.document_merchant_id is not null and s.transaction_merchant_id is not null)
            ))
          else
            (
                 (s.doc_merchant_ns is not null and s.tx_merchant_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              or (s.document_merchant_id is not null and s.transaction_merchant_id is not null)
            )
        end
      ) as merchant_applicable,
      (
        case
          when v_directional_matching then
            ( s.tx_is_in and (
                 (s.doc_customer_ns is not null and s.tx_customer_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            ))
          else
            (
                 (s.doc_customer_ns is not null and s.tx_customer_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            )
        end
      ) as customer_applicable
    from scored_components s
  ),

  scored_with_confidence as (
    select
      s.*,
      (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      )::numeric as dynamic_total_weight,
      (
        s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
        + (case when s.merchant_applicable then s.merchant_score else 0 end)
        + (case when s.customer_applicable then s.customer_score else 0 end)
      )::numeric as total_numerator,
      (coalesce(s.amount_match::int,0)
     + coalesce(s.invoice_number_match::int,0)
     + coalesce(s.order_number_match::int,0)
     + coalesce(s.date_match::int,0)
     + coalesce(s.currency_match::int,0)
     + coalesce(s.merchant_match::int,0)
     + coalesce(s.customer_match::int,0)) as signals_matched_count,
      case when (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      ) > 0 then
        round(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
              + (case when s.merchant_applicable then s.merchant_score else 0 end)
              + (case when s.customer_applicable then s.customer_score else 0 end))
            / (s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
              + (case when s.merchant_applicable then s.merchant_weight else 0 end)
              + (case when s.customer_applicable then s.customer_weight else 0 end))) * 100.0, 4)
      else 0 end as confidence_percent
    from scored_with_applicability s
  ),

  low_signal as (select * from scored_with_confidence where signals_matched_count < 1),
  below_min_confidence as (
    select * from scored_with_confidence
    where signals_matched_count >= 1 and confidence_percent < v_config_min_confidence
  ),
  base_candidates as (
    select * from scored_with_confidence
    where signals_matched_count >= 1 and confidence_percent >= v_config_min_confidence
  ),

  ranked_per_transaction as (
    select
      b.*,
      row_number() over (
        partition by b.transaction_id
        order by b.confidence_percent desc, b.total_numerator desc, b.signals_matched_count desc, b.document_id
      ) as rownum_per_transaction
    from base_candidates b
  ),
  not_top_for_transaction as (select * from ranked_per_transaction where rownum_per_transaction > 1),
  top_per_transaction as (select * from ranked_per_transaction where rownum_per_transaction = 1),

  ranked_per_document as (
    select
      t.*,
      row_number() over (
        partition by t.doc_dedup_key
        order by
          t.confidence_percent desc,
          t.total_numerator desc,
          t.signals_matched_count desc,
          abs(t.transaction_date_unix - t.document_date_unix) asc,
          t.document_id
      ) as rownum_per_document
    from top_per_transaction t
  ),
  not_top_for_document as (select * from ranked_per_document where rownum_per_document > 1),
  top_per_document as (select * from ranked_per_document where rownum_per_document = 1),

  final_candidates as (
    select *
    from top_per_document
    order by confidence_percent desc, total_numerator desc, signals_matched_count desc, transaction_id, document_id
  ),

  allowed_for_insert as (
    select f.*
    from   final_candidates f
    where  not exists (select 1 from public.reconciliations r1 where r1.transaction_id = f.transaction_id)
       and not exists (select 1 from public.reconciliations r2 where r2.document_id    = f.document_id)
  ),

  inserted_reconciliations as (
    insert into public.reconciliations (
      transaction_id, document_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary, note
    )
    with ts_base as (
      select f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             coalesce(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint as base_unix
      from allowed_for_insert f
    ),
    enrich as (
      select
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match as invoice_match,
        d.order_number_match   as order_match,
        d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        d.amount_score, d.amount_weight,
        d.date_score,   d.date_weight,
        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm, a.tx_order_norm, a.doc_order_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw, a.doc_order_raw,
        a.tx_iban_beneficiary_ns, a.tx_iban_counterparty_ns, a.doc_iban_ns,
        a.tx_iban_beneficiary_core_ns, a.tx_iban_counterparty_core_ns, a.doc_iban_core_ns,
        a.tx_is_in, a.tx_is_out
      from ts_base t
      join final_candidates d on d.transaction_id = t.transaction_id and d.document_id = t.document_id
      join attributes a       on a.transaction_id = d.transaction_id and a.document_id = d.document_id
    ),
    notes as (
      select
        e.*,
        case
          when e.document_total is not null and e.document_total <> 0
          then round(abs(e.transaction_effective_amount - e.document_total) / nullif(e.document_total,0) * 100.0, 2)
          else null
        end as amt_diff_pct,
        case
          when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix)
          else null
        end as sec_diff,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 86400.0
          else null end as day_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 3600.0
          else null end as hour_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 60.0
          else null end as minute_gap,
        case when e.amount_weight > 0 then round((e.amount_score / e.amount_weight) * 100.0, 2) end as amount_ratio_pct,
        case when e.date_weight   > 0 then round((e.date_score   / e.date_weight)   * 100.0, 0)  end as date_ratio_pct
      from enrich e
    ),
    notes_text as (
      select
        n.*,
        case
          when n.date_weight > 0 and n.date_ratio_pct <= 99 then
            trim(both ' ' from concat_ws(' ',
              case
                when n.amount_weight > 0 and n.amount_score < n.amount_weight and n.amt_diff_pct is not null
                then concat('Amount scored ', coalesce(n.amount_ratio_pct::text,'0'),
                            '% because payment vs document differed by ', n.amt_diff_pct::text, '%.')
              end,
              case
                when n.day_gap is not null
                then concat('Date scored ', coalesce(n.date_ratio_pct::text,'0'),
                            '% because dates were ', round(n.day_gap::numeric, 2)::text, ' days apart.')
              end
            ))
          else null
        end as note_text
      from notes n
    )
    select
      e.transaction_id, e.document_id, e.document_company_id,
      coalesce(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      extract(year from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      extract(quarter from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) at time zone 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          select json_agg(s) from unnest(array[
            case when e.invoice_match  then 'invoice'  end,
            case when e.amount_match   then 'amount'   end,
            case when e.date_match     then 'date'     end,
            case when e.currency_match then 'currency' end,
            case when e.merchant_match then 'merchant' end,
            case when e.customer_match then 'customer' end,
            case when e.order_match    then 'order'    end
          ]) s where s is not null
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  coalesce(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_order',     e.tx_order_norm,
        'doc_order',    coalesce(e.doc_order_raw, e.doc_order_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_iban_beneficiary',        e.tx_iban_beneficiary_ns,
        'tx_iban_counterparty',       e.tx_iban_counterparty_ns,
        'doc_iban',                   e.doc_iban_ns,
        'tx_iban_beneficiary_core',   e.tx_iban_beneficiary_core_ns,
        'tx_iban_counterparty_core',  e.tx_iban_counterparty_core_ns,
        'doc_iban_core',              e.doc_iban_core_ns,
        'tx_is_in',    e.tx_is_in,
        'tx_is_out',   e.tx_is_out,
        'direction',   case when e.tx_is_in then 'incoming' when e.tx_is_out then 'outgoing' else 'unknown' end,
        'days_between_tx_and_doc',
          case
            when e.transaction_date_unix is not null and e.document_date_unix is not null
              then (abs(e.transaction_date_unix - e.document_date_unix) / 86400)::int
            else null
          end
      )::text,
      nullif(nt.note_text,'')
    from notes_text nt
    join enrich e using (transaction_id, document_id)
    returning reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores as (
    insert into public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score,  invoice_score,  order_score,  merchant_score,  amount_score,  currency_score,  customer_score
    )
    select
      r.reconciliation_id,
      d.document_company_id as company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      d.order_number_match,
      d.merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      d.order_weight,
      d.merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      d.order_score,
      d.merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    from inserted_reconciliations r
    join scored_with_confidence d
      on d.transaction_id = r.transaction_id
     and d.document_id    = r.document_id
    returning 1
  ),

  final_counts as (
    select
      (select count(*) from top_per_transaction)      as top_per_transaction_count,
      (select count(*) from top_per_document)         as top_per_document_count,
      (select count(*) from final_candidates)         as passed_confidence_count,
      (select count(*) from inserted_reconciliations) as inserted_count,
      (select count(*) from base_candidates)          as candidates_count,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_total)    as seed_counts_total_json,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_distinct) as seed_counts_distinct_json,
      (select count(*) from transactions_scope)       as total_tx_count,
      (select count(*) from document_scope)           as total_doc_count
  )
  select
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  into
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  from final_counts;

  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', null,
    p_company_id, null, null, null,
    v_inserted_count, null, null, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered',  v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic',  v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  return jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered',  v_total_docs,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic',  v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

exception when others then
  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
  get stacked diagnostics
    v_err_state   = returned_sqlstate,
    v_err_msg     = message_text,
    v_err_detail  = pg_exception_detail,
    v_err_hint    = pg_exception_hint,
    v_err_context = pg_exception_context;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg,
    p_company_id, null, null, null,
    null, null, null, v_elapsed_ms,
    jsonb_build_object(
      'sqlstate', v_err_state,
      'detail',   v_err_detail,
      'hint',     v_err_hint,
      'context',  v_err_context
    )
  );

  return jsonb_build_object(
    'msg','error','time_ms',v_elapsed_ms,
    'transactions_scanned',0,'documents_considered',0,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',v_err_msg,'sqlstate',v_err_state
  );
end;
$$;

alter function athena_reconcile_general_v1_9_p(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v1_9_p(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v1_9_p(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v1_9_p(unknown, unknown) to service_role;

create or replace function gen_descriptor_id(descriptor text DEFAULT 'sws'::text) returns text
    language plpgsql
as
$$
BEGIN
  RETURN descriptor || '_' || encode(substring(digest(gen_random_uuid()::text, 'sha256') for 6), 'hex');
END;
$$;

alter function gen_descriptor_id(unknown) owner to postgres;

grant execute on function gen_descriptor_id(unknown) to anon;

grant execute on function gen_descriptor_id(unknown) to authenticated;

grant execute on function gen_descriptor_id(unknown) to service_role;

-- Cyclic dependencies found

create or replace function reconciliation_weights_auto_v4(p_company_id uuid DEFAULT NULL::uuid, p_days integer DEFAULT 365, p_min_rows integer DEFAULT 50, p_conf_min numeric DEFAULT 80, p_half_life_days integer DEFAULT 365, p_verified_only boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_rows_upserted int := 0;
  v_companies     int := 0;
BEGIN
  WITH
  base AS (
    SELECT
      r.company_id,
      r.confidence,
      r.verified,
      s.date_match::int      AS hit_date,
      s.invoice_match::int   AS hit_inv,
      s.order_match::int     AS hit_ord,
      s.merchant_match::int  AS hit_mer,
      (s.amount_match)::int  AS hit_amt,
      s.currency_match::int  AS hit_cur,
      s.customer_match::int  AS hit_cus,
      s.created_at
    FROM public.reconciliations_match_scores s
    JOIN public.reconciliations r ON r.reconciliation_id = s.reconciliation_id
    WHERE s.created_at >= now() - make_interval(days => p_days)
      AND (p_company_id IS NULL OR r.company_id = p_company_id)
      AND (NOT p_verified_only OR r.verified IS TRUE)
  ),

  -- recency * confidence weight (with tiny floor)
  weighted AS (
    SELECT
      company_id,
      GREATEST(
        0.05,
        EXP(-LN(2) * (EXTRACT(EPOCH FROM (now() - created_at))/86400.0)
            / NULLIF(p_half_life_days,0))
        * CASE
            WHEN p_conf_min IS NULL OR p_conf_min = 0
              THEN LEAST(1.0, GREATEST(0.0, COALESCE(confidence, 50)::numeric / 100.0))
            ELSE LEAST(1.0, GREATEST(0.0, (COALESCE(confidence, p_conf_min)::numeric - p_conf_min) / 20.0))
          END
      ) AS w,
      hit_date, hit_inv, hit_ord, hit_mer, hit_amt, hit_cur, hit_cus
    FROM base
  ),

  agg AS (
    SELECT
      company_id,
      SUM(w)            AS n_w,
      SUM(w*hit_date)   AS x_date,
      SUM(w*hit_inv)    AS x_inv,
      SUM(w*hit_ord)    AS x_ord,
      SUM(w*hit_mer)    AS x_mer,
      SUM(w*hit_amt)    AS x_amt,
      SUM(w*hit_cur)    AS x_cur,
      SUM(w*hit_cus)    AS x_cus
    FROM weighted
    GROUP BY company_id
  ),

  eligible AS (
    SELECT * FROM agg WHERE n_w >= p_min_rows
  ),

  wilson AS (
    SELECT company_id, n_w, 1.96::numeric AS z,
           x_date, x_inv, x_ord, x_mer, x_amt, x_cur, x_cus
    FROM eligible
  ),

  rates AS (
    SELECT
      company_id, n_w,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_date/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_date/n_w)*(1 - (x_date/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_date,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_inv/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_inv/n_w)*(1 - (x_inv/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_inv,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_ord/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_ord/n_w)*(1 - (x_ord/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_ord,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_mer/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_mer/n_w)*(1 - (x_mer/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_mer,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_amt/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_amt/n_w)*(1 - (x_amt/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_amt,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_cur/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_cur/n_w)*(1 - (x_cur/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_cur,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_cus/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_cus/n_w)*(1 - (x_cus/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_cus
    FROM wilson
  ),

  -- Map LBs into anchored bands (importance-first)
  desired AS (
    SELECT company_id, 'date'            AS weight_key, ROUND(0.80 + 0.40 * lb_date, 2) AS final_value FROM rates
    UNION ALL
    SELECT company_id, 'invoice_number',  ROUND(1.60 + 0.20 * lb_inv,  2) FROM rates
    UNION ALL
    SELECT company_id, 'order_number',    ROUND(1.00 + 0.50 * lb_ord,  2) FROM rates
    UNION ALL
    SELECT company_id, 'merchant_id',     ROUND(1.00 + 0.40 * lb_mer,  2) FROM rates
    UNION ALL
    SELECT company_id, 'amount_total',    ROUND(1.20 + 0.50 * lb_amt,  2) FROM rates
    UNION ALL
    SELECT company_id, 'currency',        ROUND(0.05 + 0.15 * lb_cur,  2) FROM rates
    UNION ALL
    SELECT company_id, 'customer_name',   ROUND(0.40 + 0.40 * lb_cus,  2) FROM rates
  ),

  upserted AS (
    INSERT INTO public.reconciliations_weight_registry
      (company_id, merchant_id, weight_key, weight_value, active, created_at)
    SELECT
      d.company_id, NULL, d.weight_key,
      LEAST(2.00, GREATEST(0.01, d.final_value)), TRUE, now()
    FROM desired d
    ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
    DO UPDATE SET
      weight_value = EXCLUDED.weight_value,
      active       = TRUE,
      created_at   = now()
    RETURNING 1
  )

  SELECT COUNT(*) INTO v_rows_upserted FROM upserted;

  SELECT COUNT(*) INTO v_companies FROM eligible;

  RETURN jsonb_build_object(
    'status',               'ok',
    'companies_considered', v_companies,
    'rows_upserted',        COALESCE(v_rows_upserted,0),
    'lookback_days',        p_days,
    'min_effective_rows',   p_min_rows,
    'conf_min',             p_conf_min,
    'half_life_days',       p_half_life_days,
    'verified_only',        p_verified_only,
    'filtered_company',     p_company_id::text
  );
END;
$$;

alter function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

create or replace function reconciliation_weights_auto_v4(p_company_id uuid DEFAULT NULL::uuid, p_days integer DEFAULT 90, p_min_rows numeric DEFAULT 200, p_conf_min numeric DEFAULT 80, p_half_life_days numeric DEFAULT 45, p_verified_only boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_rows_upserted int := 0;
  v_companies     int := 0;
BEGIN
  WITH
  base AS (
    SELECT
      r.company_id,
      r.confidence,
      r.verified,
      s.date_match::int      AS hit_date,
      s.invoice_match::int   AS hit_inv,
      s.order_match::int     AS hit_ord,
      s.merchant_match::int  AS hit_mer,
      (s.amount_match)::int  AS hit_amt,
      s.currency_match::int  AS hit_cur,
      s.customer_match::int  AS hit_cus,
      s.created_at
    FROM public.reconciliations_match_scores s
    JOIN public.reconciliations r ON r.reconciliation_id = s.reconciliation_id
    WHERE s.created_at >= now() - make_interval(days => p_days)
      AND (p_company_id IS NULL OR r.company_id = p_company_id)
      AND (NOT p_verified_only OR r.verified IS TRUE)
  ),

  -- recency * confidence weight (with tiny floor)
  weighted AS (
    SELECT
      company_id,
      GREATEST(
        0.05,
        EXP(-LN(2) * (EXTRACT(EPOCH FROM (now() - created_at))/86400.0)
            / NULLIF(p_half_life_days,0))
        * CASE
            WHEN p_conf_min IS NULL OR p_conf_min = 0
              THEN LEAST(1.0, GREATEST(0.0, COALESCE(confidence, 50)::numeric / 100.0))
            ELSE LEAST(1.0, GREATEST(0.0, (COALESCE(confidence, p_conf_min)::numeric - p_conf_min) / 20.0))
          END
      ) AS w,
      hit_date, hit_inv, hit_ord, hit_mer, hit_amt, hit_cur, hit_cus
    FROM base
  ),

  agg AS (
    SELECT
      company_id,
      SUM(w)            AS n_w,
      SUM(w*hit_date)   AS x_date,
      SUM(w*hit_inv)    AS x_inv,
      SUM(w*hit_ord)    AS x_ord,
      SUM(w*hit_mer)    AS x_mer,
      SUM(w*hit_amt)    AS x_amt,
      SUM(w*hit_cur)    AS x_cur,
      SUM(w*hit_cus)    AS x_cus
    FROM weighted
    GROUP BY company_id
  ),

  eligible AS (
    SELECT * FROM agg WHERE n_w >= p_min_rows
  ),

  wilson AS (
    SELECT company_id, n_w, 1.96::numeric AS z,
           x_date, x_inv, x_ord, x_mer, x_amt, x_cur, x_cus
    FROM eligible
  ),

  rates AS (
    SELECT
      company_id, n_w,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_date/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_date/n_w)*(1 - (x_date/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_date,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_inv/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_inv/n_w)*(1 - (x_inv/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_inv,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_ord/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_ord/n_w)*(1 - (x_ord/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_ord,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_mer/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_mer/n_w)*(1 - (x_mer/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_mer,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_amt/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_amt/n_w)*(1 - (x_amt/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_amt,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_cur/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_cur/n_w)*(1 - (x_cur/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_cur,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_cus/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_cus/n_w)*(1 - (x_cus/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_cus
    FROM wilson
  ),

  -- Map LBs into anchored bands (importance-first)
  desired AS (
    SELECT company_id, 'date'            AS weight_key, ROUND(0.80 + 0.40 * lb_date, 2) AS final_value FROM rates
    UNION ALL
    SELECT company_id, 'invoice_number',  ROUND(1.60 + 0.20 * lb_inv,  2) FROM rates
    UNION ALL
    SELECT company_id, 'order_number',    ROUND(1.00 + 0.50 * lb_ord,  2) FROM rates
    UNION ALL
    SELECT company_id, 'merchant_id',     ROUND(1.00 + 0.40 * lb_mer,  2) FROM rates
    UNION ALL
    SELECT company_id, 'amount_total',    ROUND(1.20 + 0.50 * lb_amt,  2) FROM rates
    UNION ALL
    SELECT company_id, 'currency',        ROUND(0.05 + 0.15 * lb_cur,  2) FROM rates
    UNION ALL
    SELECT company_id, 'customer_name',   ROUND(0.40 + 0.40 * lb_cus,  2) FROM rates
  ),

  upserted AS (
    INSERT INTO public.reconciliations_weight_registry
      (company_id, merchant_id, weight_key, weight_value, active, created_at)
    SELECT
      d.company_id, NULL, d.weight_key,
      LEAST(2.00, GREATEST(0.01, d.final_value)), TRUE, now()
    FROM desired d
    ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
    DO UPDATE SET
      weight_value = EXCLUDED.weight_value,
      active       = TRUE,
      created_at   = now()
    RETURNING 1
  )

  SELECT COUNT(*) INTO v_rows_upserted FROM upserted;

  SELECT COUNT(*) INTO v_companies FROM eligible;

  RETURN jsonb_build_object(
    'status',               'ok',
    'companies_considered', v_companies,
    'rows_upserted',        COALESCE(v_rows_upserted,0),
    'lookback_days',        p_days,
    'min_effective_rows',   p_min_rows,
    'conf_min',             p_conf_min,
    'half_life_days',       p_half_life_days,
    'verified_only',        p_verified_only,
    'filtered_company',     p_company_id::text
  );
END;
$$;

alter function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

