          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.transaction_invoice_number,
        'doc_invoice',  e.document_invoice_number,
        'tx_order',     e.transaction_order_id,
        'doc_order',    e.document_order_id,
        '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
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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,
      r.confidence,
      d.total_numerator,
      weights_total_possible,
      d.date_match,
      d.invoice_match,
      d.order_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 final_candidates d ON d.transaction_id = r.transaction_id AND d.document_id = r.document_id
    RETURNING 1
  ),

  logging_base AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id AS company_id,
      s.confidence_percent, s.date_match, s.order_match, s.amount_match, s.invoice_match,
      s.currency_match, s.customer_match, s.merchant_match,
      s.amount_score, s.invoice_score, s.order_score, s.date_score, s.currency_score, s.merchant_score, s.customer_score,
      s.amount_weight, s.invoice_weight, s.order_weight, s.date_weight, s.currency_weight, s.merchant_weight, s.customer_weight,
      s.transaction_effective_amount, s.document_total, s.amount_direction_ok,
      CASE WHEN s.transaction_date_unix IS NOT NULL AND s.document_date_unix IS NOT NULL
           THEN (ABS(s.transaction_date_unix - s.document_date_unix) / 86400.0)::numeric
           ELSE NULL::numeric END AS date_diff_days
    FROM scored_with_confidence s
  ),

  logging_matched_rows AS (
    INSERT INTO public.reconciliation_activity (
      reconciliation_id, run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      i.reconciliation_id, log_run_id, log_run_unix_time, lb.company_id, i.transaction_id, i.document_id,
      'matched'::text, NULL::text,
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                              THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                              ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  logging_skipped_union AS (
    INSERT INTO public.reconciliations_activity (
      run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','not_top_for_tx',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM not_top_for_transaction x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL

    SELECT
      log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','not_top_for_doc',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM not_top_for_document x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL

    SELECT
      log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','below_min_confidence',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM below_min_confidence x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL

    SELECT
      log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','no_signals',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM low_signal x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    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
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count
  INTO
    summary_top_per_transaction_count,
    summary_top_per_document_count,
    summary_passed_confidence_count,
    v_inserted_count,
    v_candidates_count
  FROM final_counts;

  -- end log
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - 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 (
    log_run_id, engine_version, 'run_end', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    v_inserted_count, NULL, NULL, elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'top_per_tx', summary_top_per_transaction_count,
      'top_per_doc', summary_top_per_document_count,
      'passed_confidence', summary_passed_confidence_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               status_message,
    'time_ms',           elapsed_ms,
    'top_per_tx',        summary_top_per_transaction_count,
    'top_per_doc',       summary_top_per_document_count,
    'passed_confidence', summary_passed_confidence_count
  );

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

  GET STACKED DIAGNOSTICS
    err_state   = RETURNED_SQLSTATE,
    err_msg     = MESSAGE_TEXT,
    err_detail  = PG_EXCEPTION_DETAIL,
    err_hint    = PG_EXCEPTION_HINT,
    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 (
    log_run_id, engine_version, 'run_error', 'function', 'error', err_msg,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, elapsed_ms,
    jsonb_build_object(
      'sqlstate', err_state,
      'detail',   err_detail,
      'hint',     err_hint,
      'context',  err_context
    )
  );

  RETURN jsonb_build_object(
    'msg','error','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',err_msg,'sqlstate',err_state
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1_9_b(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
--------------------------------
-- Only a historic version of ciaran knows this code - and the devil himself
-- dont try to get me to explain ths
-- dont even look at me
-- dont bring it up
-- dont even try to look over this horrid voodoo
-------------------------------
DECLARE
  -- run status/meta
  v_status_message text := 'success';
  v_engine_version text := '1.9b';

  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_seed_cap_per_match_type   int := 10;
  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;

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

  -- matching options
  v_directional_matching boolean := FALSE; -- DIRECTIONAL AWARENESS FOR MERCHANT AND CUSTOMER MATCHING

  -- errors
  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;

BEGIN
  -- load 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,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  -- derive 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;

  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;

  -- resolve 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,
        'seed_cap_per_criterion', v_seed_cap_per_match_type,
        'directional_matching', v_directional_matching
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  -- pipeline
  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,
      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,
      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,

      -- normalized invoice/order: from field or extracted from remittance/description
      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_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '') AS tx_iban_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
  ),

  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE 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)
          OR (d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm)
          OR (d.currency = t.currency AND d.merchant_id   = t.merchant_id)
          OR (d.currency = t.currency AND d.customer_name = t.name_consumer)

          OR (((NOT v_directional_matching) OR (t.tx_is_out)) 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.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))

          OR (((NOT v_directional_matching) OR (t.tx_is_in)) 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.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))

          OR (((NOT v_directional_matching) OR (t.tx_is_out)) 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 t.tx_iban_ns IS NOT NULL AND d.doc_iban_ns IS NOT NULL
              AND t.tx_iban_ns = d.doc_iban_ns)

          OR (d.currency = t.currency AND (
               d.total = t.effective_amount
            OR ( t.effective_amount IS NOT NULL
                 AND d.total IS NOT NULL
                 AND ABS(d.total - t.effective_amount) <= d.total * v_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number_norm IS NOT NULL AND t.invoice_nr_norm IS NOT NULL AND d.invoice_number_norm = t.invoice_nr_norm THEN 0
          WHEN d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(COALESCE(d.document_date_unix,0) - t.completed_date) ASC,
        d.document_id
      LIMIT (v_seed_cap_per_match_type * 6)
    ) d ON TRUE
  ),

  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_ns,     d.doc_iban_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,

      (
        (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 (
             ((NOT v_directional_matching) OR a.tx_is_out)
             AND 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 (
             ((NOT v_directional_matching) OR a.tx_is_out)
             AND a.tx_iban_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
             AND a.tx_iban_ns = a.doc_iban_ns
           )
      ) AS merchant_match,

      (
        ((NOT v_directional_matching) OR 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)
      ) AS customer_match,

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

      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
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.doc_dedup_key,

      e.invoice_number_match,
      e.order_number_match,
      e.date_match,
      e.currency_match,
      e.merchant_match,
      e.customer_match,

      e.transaction_effective_amount,
      e.document_total,
      e.transaction_date_unix,
      e.document_date_unix,

      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,

      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * v_amount_tolerance_fraction
        THEN TRUE
        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,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN GREATEST(
            0,
            LEAST(
              w.amount_weight,
              w.amount_weight * (1 - (
                ABS(m.transaction_effective_amount - m.document_total)
                / NULLIF(m.document_total * v_amount_tolerance_fraction, 0)
              ))
            )
          )
          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_match,
      m.order_number_match    AS order_match,
      m.date_match            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.date_match::int,0)           * w.date_weight)::numeric     AS date_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_confidence AS (
    SELECT
      s.transaction_id,
      s.document_id,
      s.document_company_id,
      s.doc_dedup_key,
      s.transaction_date_unix,
      s.document_date_unix,
      s.amount_direction_ok,

      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,

      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_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 v_weights_total_possible > 0 THEN
        ROUND(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                / v_weights_total_possible) * 100.0, 4)
      ELSE 0 END AS confidence_percent,

      s.amount_match,
      s.invoice_match,
      s.order_match,
      s.date_match,
      s.currency_match,
      s.merchant_match,
      s.customer_match,

      s.date_weight,
      s.invoice_weight,
      s.order_weight,
      s.merchant_weight,
      s.amount_weight,
      s.currency_weight,
      s.customer_weight,

      s.date_score,
      s.invoice_score,
      s.order_score,
      s.merchant_score,
      s.amount_score,
      s.currency_score,
      s.customer_score,

      s.transaction_effective_amount,
      s.document_total
    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),

  -- enforce "one per dedup group" by ranking on doc_dedup_key
  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
    LIMIT 2000
  ),

  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_match, d.order_match, d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        d.transaction_effective_amount, d.document_total,
        d.transaction_date_unix, d.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
      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
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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,
      r.confidence,
      d.total_numerator,
      v_weights_total_possible,
      d.date_match,
      d.invoice_match,
      d.order_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 final_candidates d ON d.transaction_id = r.transaction_id AND d.document_id = r.document_id
    RETURNING 1
  ),

  logging_base AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id AS company_id,
      s.confidence_percent, s.date_match, s.order_match, s.amount_match, s.invoice_match,
      s.currency_match, s.customer_match, s.merchant_match,
      s.amount_score, s.invoice_score, s.order_score, s.date_score, s.currency_score, s.merchant_score, s.customer_score,
      s.amount_weight, s.invoice_weight, s.order_weight, s.date_weight, s.currency_weight, s.merchant_weight, s.customer_weight,
      s.transaction_effective_amount, s.document_total, s.amount_direction_ok,
      CASE WHEN s.transaction_date_unix IS NOT NULL AND s.document_date_unix IS NOT NULL
           THEN (ABS(s.transaction_date_unix - s.document_date_unix) / 86400.0)::numeric
           ELSE NULL::numeric END AS date_diff_days
    FROM scored_with_confidence s
  ),

  logging_matched_rows AS (
    INSERT INTO public.reconciliation_activity (
      reconciliation_id, run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      i.reconciliation_id, v_log_run_id, v_log_run_unix_time, lb.company_id, i.transaction_id, i.document_id,
      'matched'::text, NULL::text,
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                              THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                              ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity
    RETURNING 1
  ),

  logging_skipped_union AS (
    INSERT INTO public.reconciliations_activity (
      run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','not_top_for_tx',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM not_top_for_transaction x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity

    UNION ALL

    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','not_top_for_doc',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM not_top_for_document x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity

    UNION ALL

    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','below_min_confidence',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM below_min_confidence x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity

    UNION ALL

    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','no_signals',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM low_signal x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity
    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
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count
  INTO
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count
  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,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count
  );

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,'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_b(unknown, unknown) owner to postgres;

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

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

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

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

  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; -- full date weight up to 30 days, then decay to 0 at hard cap
  v_seed_cap_per_match_type   int := 10;
  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;

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

  -- matching options
  v_directional_matching boolean := FALSE;

  -- errors
  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;

BEGIN
  -- load 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,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  -- derive 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;

  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;

  -- resolve 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,
        'seed_cap_per_criterion', v_seed_cap_per_match_type,
        'directional_matching', v_directional_matching
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  -- pipeline
  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,
      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,
      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,
      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_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '') AS tx_iban_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
  ),

  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE 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)
          OR (d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm)
          OR (d.currency = t.currency AND d.merchant_id   = t.merchant_id)
          OR (d.currency = t.currency AND d.customer_name = t.name_consumer)

          OR (((NOT v_directional_matching) OR (t.tx_is_out)) 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.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))

          OR (((NOT v_directional_matching) OR (t.tx_is_in)) 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.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))

          OR (((NOT v_directional_matching) OR (t.tx_is_out)) 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 t.tx_iban_ns IS NOT NULL AND d.doc_iban_ns IS NOT NULL
              AND t.tx_iban_ns = d.doc_iban_ns)

          OR (d.currency = t.currency AND (
               d.total = t.effective_amount
            OR ( t.effective_amount IS NOT NULL
                 AND d.total IS NOT NULL
                 AND ABS(d.total - t.effective_amount) <= d.total * v_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number_norm IS NOT NULL AND t.invoice_nr_norm IS NOT NULL AND d.invoice_number_norm = t.invoice_nr_norm THEN 0
          WHEN d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(COALESCE(d.document_date_unix,0) - t.completed_date) ASC,
        d.document_id
      LIMIT (v_seed_cap_per_match_type * 6)
    ) d ON TRUE
  ),

  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_ns,     d.doc_iban_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,

      (
        (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 (
             ((NOT v_directional_matching) OR a.tx_is_out)
             AND 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 (
             ((NOT v_directional_matching) OR a.tx_is_out)
             AND a.tx_iban_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
             AND a.tx_iban_ns = a.doc_iban_ns
           )
      ) AS merchant_match,

      (
        ((NOT v_directional_matching) OR 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)
      ) AS customer_match,

      -- date hard cap
      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
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.doc_dedup_key,

      e.invoice_number_match,
      e.order_number_match,
      e.date_within_cap,
      e.currency_match,
      e.merchant_match,
      e.customer_match,

      e.transaction_effective_amount,
      e.document_total,
      e.transaction_date_unix,
      e.document_date_unix,

      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,

      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * v_amount_tolerance_fraction
        THEN TRUE
        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,

      -- amount score (unchanged)
      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN GREATEST(
            0,
            LEAST(
              w.amount_weight,
              w.amount_weight * (1 - (
                ABS(m.transaction_effective_amount - m.document_total)
                / NULLIF(m.document_total * v_amount_tolerance_fraction, 0)
              ))
            )
          )
          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_match,
      m.order_number_match    AS order_match,

      -- date decay: full weight up to 30 days, then linear decay to 0 at hard cap
      CASE
        WHEN m.date_within_cap AND m.transaction_date_unix IS NOT NULL AND m.document_date_unix IS NOT NULL THEN
          (
            CASE
              WHEN ABS(m.transaction_date_unix - m.document_date_unix) <= (v_date_full_weight_grace_days * 86400)
                THEN w.date_weight
              ELSE
                -- linear factor from 1 at 30d to 0 at cap
                w.date_weight * GREATEST(
                  0,
                  1 - (
                    ((ABS(m.transaction_date_unix - m.document_date_unix) / 86400.0)
                      - v_date_full_weight_grace_days)::numeric
                    / NULLIF((v_config_date_window_days - v_date_full_weight_grace_days)::numeric, 0)
                  )
                )
            END
          )
        ELSE 0
      END::numeric AS date_score,

      -- keep a boolean for counting matched signals (within hard cap counts as a date signal)
      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_confidence AS (
    SELECT
      s.transaction_id,
      s.document_id,
      s.document_company_id,
      s.doc_dedup_key,
      s.transaction_date_unix,
      s.document_date_unix,
      s.amount_direction_ok,

      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,

      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_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 v_weights_total_possible > 0 THEN
        ROUND(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                / v_weights_total_possible) * 100.0, 4)
      ELSE 0 END AS confidence_percent,

      s.amount_match,
      s.invoice_match,
      s.order_match,
      s.date_match,
      s.currency_match,
      s.merchant_match,
      s.customer_match,

      s.date_weight,
      s.invoice_weight,
      s.order_weight,
      s.merchant_weight,
      s.amount_weight,
      s.currency_weight,
      s.customer_weight,

      s.date_score,
      s.invoice_score,
      s.order_score,
      s.merchant_score,
      s.amount_score,
      s.currency_score,
      s.customer_score,

      s.transaction_effective_amount,
      s.document_total
    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
    LIMIT 2000
  ),

  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_match, d.order_match, d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        d.transaction_effective_amount, d.document_total,
        d.transaction_date_unix, d.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
      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
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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,
      r.confidence,
      d.total_numerator,
      v_weights_total_possible,
      d.date_match,
      d.invoice_match,
      d.order_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 final_candidates d ON d.transaction_id = r.transaction_id AND d.document_id = r.document_id
    RETURNING 1
  ),

  logging_base AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id AS company_id,
      s.confidence_percent, s.date_match, s.order_match, s.amount_match, s.invoice_match,
      s.currency_match, s.customer_match, s.merchant_match,
      s.amount_score, s.invoice_score, s.order_score, s.date_score, s.currency_score, s.merchant_score, s.customer_score,
      s.amount_weight, s.invoice_weight, s.order_weight, s.date_weight, s.currency_weight, s.merchant_weight, s.customer_weight,
      s.transaction_effective_amount, s.document_total, s.amount_direction_ok,
      CASE WHEN s.transaction_date_unix IS NOT NULL AND s.document_date_unix IS NOT NULL
           THEN (ABS(s.transaction_date_unix - s.document_date_unix) / 86400.0)::numeric
           ELSE NULL::numeric END AS date_diff_days
    FROM scored_with_confidence s
  ),

  logging_matched_rows AS (
    INSERT INTO public.reconciliation_activity (
      reconciliation_id, run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      i.reconciliation_id, v_log_run_id, v_log_run_unix_time, lb.company_id, i.transaction_id, i.document_id,
      'matched'::text, NULL::text,
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                              THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                              ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity
    RETURNING 1
  ),

  logging_skipped_union AS (
    INSERT INTO public.reconciliations_activity (
      run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','not_top_for_tx',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM not_top_for_transaction x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity

    UNION ALL

    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','not_top_for_doc',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM not_top_for_document x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity

    UNION ALL

    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','below_min_confidence',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM below_min_confidence x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity

    UNION ALL

    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','no_signals',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM low_signal x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity
    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
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count
  INTO
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count
  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,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count
  );

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,'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_c(unknown, unknown) owner to postgres;

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

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

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

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

  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; -- full date weight up to 30 days, then decay to 0 at hard cap
  v_seed_cap_per_match_type   int := 10;
  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;

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

  -- matching options
  v_directional_matching boolean := FALSE;  -- keep directional behaviour on

  -- errors
  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;

BEGIN
  -- load 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,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  -- derive 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;

  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;

  -- resolve 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,
        'seed_cap_per_criterion', v_seed_cap_per_match_type,
        'directional_matching', v_directional_matching
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  -- pipeline
  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,
      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,
      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,
      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_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '') AS tx_iban_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
  ),

  -- SEEDING: remove date-only and currency-only seeds; keep currency as conjunct with strong signals.
  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          -- Exact invoice/order IDs
          (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 (d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm)

          -- Directional party equality (+currency)
          OR (d.currency = t.currency AND ((NOT v_directional_matching) OR t.tx_is_out) AND d.merchant_id   IS NOT NULL AND t.merchant_id IS NOT NULL AND d.merchant_id   = t.merchant_id)
          OR (d.currency = t.currency AND ((NOT v_directional_matching) OR t.tx_is_in ) AND d.customer_name IS NOT NULL AND t.name_consumer IS NOT NULL AND d.customer_name = t.name_consumer)

          -- IBAN equality (+currency), directional OUT
          OR (d.currency = t.currency AND ((NOT v_directional_matching) OR t.tx_is_out) AND d.doc_iban_ns IS NOT NULL AND t.tx_iban_ns IS NOT NULL AND d.doc_iban_ns = t.tx_iban_ns)

          -- Amount exact (+currency)
          OR (d.currency = t.currency AND d.total = t.effective_amount)

          -- Amount within tolerance, *sargable* (+currency)
          OR (d.currency = t.currency
              AND t.effective_amount IS NOT NULL
              AND d.total BETWEEN t.effective_amount * (1 - v_amount_tolerance_fraction)
                              AND t.effective_amount * (1 + v_amount_tolerance_fraction))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number_norm IS NOT NULL AND t.invoice_nr_norm IS NOT NULL AND d.invoice_number_norm = t.invoice_nr_norm THEN 0
          WHEN d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        d.document_id
      LIMIT (v_seed_cap_per_match_type * 6)
    ) d ON TRUE
  ),

  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_ns,     d.doc_iban_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,

      (
        (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 (
             ((NOT v_directional_matching) OR a.tx_is_out)
             AND 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 (
             ((NOT v_directional_matching) OR a.tx_is_out)
             AND a.tx_iban_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
             AND a.tx_iban_ns = a.doc_iban_ns
           )
      ) AS merchant_match,

      (
        ((NOT v_directional_matching) OR 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)
      ) AS customer_match,

      -- date is scoring-only (hard cap here just gates the date signal/score, not seeding)
      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
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.doc_dedup_key,

      e.invoice_number_match,
      e.order_number_match,
      e.date_within_cap,
      e.currency_match,
      e.merchant_match,
      e.customer_match,

      e.transaction_effective_amount,
      e.document_total,
      e.transaction_date_unix,
      e.document_date_unix,

      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,

      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * v_amount_tolerance_fraction
        THEN TRUE
        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,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN GREATEST(
            0,
            LEAST(
              w.amount_weight,
              w.amount_weight * (1 - (
                ABS(m.transaction_effective_amount - m.document_total)
                / NULLIF(m.document_total * v_amount_tolerance_fraction, 0)
              ))
            )
          )
          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_match,
      m.order_number_match    AS order_match,

      -- date scoring with grace then linear decay to 0 at hard cap
      CASE
        WHEN m.date_within_cap AND m.transaction_date_unix IS NOT NULL AND m.document_date_unix IS NOT NULL THEN
          (
            CASE
              WHEN ABS(m.transaction_date_unix - m.document_date_unix) <= (v_date_full_weight_grace_days * 86400)
                THEN w.date_weight
              ELSE
                w.date_weight * GREATEST(
                  0,
                  1 - (
                    ((ABS(m.transaction_date_unix - m.document_date_unix) / 86400.0)
                      - v_date_full_weight_grace_days)::numeric
                    / NULLIF((v_config_date_window_days - v_date_full_weight_grace_days)::numeric, 0)
                  )
                )
            END
          )
        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_confidence AS (
    SELECT
      s.transaction_id,
      s.document_id,
      s.document_company_id,
      s.doc_dedup_key,
      s.transaction_date_unix,
      s.document_date_unix,
      s.amount_direction_ok,

      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,

      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_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 v_weights_total_possible > 0 THEN
        ROUND(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                / v_weights_total_possible) * 100.0, 4)
      ELSE 0 END AS confidence_percent,

      s.amount_match,
      s.invoice_match,
      s.order_match,
      s.date_match,
      s.currency_match,
      s.merchant_match,
      s.customer_match,

      s.date_weight,
      s.invoice_weight,
      s.order_weight,
      s.merchant_weight,
      s.amount_weight,
      s.currency_weight,
      s.customer_weight,

      s.date_score,
      s.invoice_score,
      s.order_score,
      s.merchant_score,
      s.amount_score,
      s.currency_score,
      s.customer_score,

      s.transaction_effective_amount,
      s.document_total
    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
    LIMIT 2000
  ),

  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_match, d.order_match, d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        d.transaction_effective_amount, d.document_total,
        d.transaction_date_unix, d.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
      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
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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,
      r.confidence,
      d.total_numerator,
      v_weights_total_possible,
      d.date_match,
      d.invoice_match,
      d.order_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 final_candidates d ON d.transaction_id = r.transaction_id AND d.document_id = r.document_id
    RETURNING 1
  ),

  logging_base AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id AS company_id,
      s.confidence_percent, s.date_match, s.order_match, s.amount_match, s.invoice_match,
      s.currency_match, s.customer_match, s.merchant_match,
      s.amount_score, s.invoice_score, s.order_score, s.date_score, s.currency_score, s.merchant_score, s.customer_score,
      s.amount_weight, s.invoice_weight, s.order_weight, s.date_weight, s.currency_weight, s.merchant_weight, s.customer_weight,
      s.transaction_effective_amount, s.document_total, s.amount_direction_ok,
      CASE WHEN s.transaction_date_unix IS NOT NULL AND s.document_date_unix IS NOT NULL
           THEN (ABS(s.transaction_date_unix - s.document_date_unix) / 86400.0)::numeric
           ELSE NULL::numeric END AS date_diff_days
    FROM scored_with_confidence s
  ),

  logging_matched_rows AS (
    INSERT INTO public.reconciliation_activity (
      reconciliation_id, run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      i.reconciliation_id, v_log_run_id, v_log_run_unix_time, lb.company_id, i.transaction_id, i.document_id,
      'matched'::text, NULL::text,
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                              THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                              ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity
    RETURNING 1
  ),

  logging_skipped_union AS (
    INSERT INTO public.reconciliations_activity (
      run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','not_top_for_tx',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM not_top_for_transaction x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity

    UNION ALL

    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','not_top_for_doc',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM not_top_for_document x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity

    UNION ALL

    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','below_min_confidence',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM below_min_confidence x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity

    UNION ALL

    SELECT
      v_log_run_id, v_log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
      'skipped','no_signals',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match, 'order', lb.order_match, 'amount', lb.amount_match, 'invoice', lb.invoice_match,
          'currency', lb.currency_match, 'customer', lb.customer_match, 'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score, 'invoice', lb.invoice_score, 'order', lb.order_score,
          'date', lb.date_score, 'currency', lb.currency_score, 'merchant', lb.merchant_score, 'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight, 'invoice', lb.invoice_weight, 'order', lb.order_weight,
          'date', lb.date_weight, 'currency', lb.currency_weight, 'merchant', lb.merchant_weight, 'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM low_signal x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE v_config_log_activity
    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
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count
  INTO
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count
  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,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count
  );

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,'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_d(unknown, unknown) owner to postgres;

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

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

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

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

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

  -- stage timing
  v_t_prev timestamptz := v_start_time;
  v_t_now  timestamptz;
  v_timings jsonb := '[]'::jsonb;

  -- 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; -- full date weight up to 30 days, then decay to 0 at hard cap
  v_seed_cap_per_match_type   int := 10;   -- meta only
  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;
  v_candidate_pairs_cnt  bigint := 0;
  v_distinct_pairs_cnt   bigint := 0;

  -- scope counts
  v_doc_scope_count bigint := 0;
  v_tx_scope_count  bigint := 0;

  -- per-seed candidate counts
  v_seed_invoice_cnt     bigint := 0;
  v_seed_order_cnt       bigint := 0;
  v_seed_merchantid_cnt  bigint := 0;
  v_seed_customer_ns_cnt bigint := 0;
  v_seed_merchant_ns_cnt bigint := 0;
  v_seed_iban_cnt        bigint := 0;
  v_seed_amount_cnt      bigint := 0;

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

  -- matching options
  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
  -- load 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,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  v_t_now := clock_timestamp();
  v_timings := v_timings || jsonb_build_object('stage','load_config','ms', FLOOR(EXTRACT(EPOCH FROM (v_t_now - v_t_prev))*1000)::bigint);
  v_t_prev := v_t_now;

  -- derive 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;

  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;

  -- resolve 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;

  v_t_now := clock_timestamp();
  v_timings := v_timings || jsonb_build_object('stage','resolve_weights','ms', FLOOR(EXTRACT(EPOCH FROM (v_t_now - v_t_prev))*1000)::bigint);
  v_t_prev := v_t_now;

  -- 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,
        'seed_cap_per_criterion', v_seed_cap_per_match_type,
        'directional_matching', v_directional_matching
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  v_t_now := clock_timestamp();
  v_timings := v_timings || jsonb_build_object('stage','run_start_log','ms', FLOOR(EXTRACT(EPOCH FROM (v_t_now - v_t_prev))*1000)::bigint);
  v_t_prev := v_t_now;

  ---------------------------------------------------------------------------
  -- lightweight diagnostic counts (no temp tables)
  ---------------------------------------------------------------------------
  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,
      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
    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,
      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_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '') AS tx_iban_ns,
      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
  )
  SELECT
    (SELECT COUNT(*) FROM document_scope),
    (SELECT COUNT(*) FROM transactions_scope)
  INTO v_doc_scope_count, v_tx_scope_count;

  v_t_now := clock_timestamp();
  v_timings := v_timings || jsonb_build_object('stage','scope_counts','ms', FLOOR(EXTRACT(EPOCH FROM (v_t_now - v_t_prev))*1000)::bigint,
                                               'doc_scope', v_doc_scope_count, 'tx_scope', v_tx_scope_count);
  v_t_prev := v_t_now;

  -- per-seed pair counts (single statement to reuse scopes)
  WITH
  document_scope AS MATERIALIZED (
    SELECT
      d.document_id, d.company_id, d.total, d.currency,
      NULLIF(public.norm_code(d.invoice_number), '') AS invoice_number_norm,
      NULLIF(public.norm_code(d.order_id), '')       AS order_id_norm,
      d.merchant_id,
      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
    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.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.merchant_id,
      (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_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '') AS tx_iban_ns,
      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
  )
  SELECT
    -- invoice
    (SELECT COUNT(*) 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) AS c1,
    -- order
    (SELECT COUNT(*) 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) AS c2,
    -- merchant_id + currency (out)
    (SELECT COUNT(*) FROM transactions_scope t JOIN document_scope d
       ON d.company_id = t.company_id::uuid
      AND d.currency = t.currency
      AND d.merchant_id IS NOT NULL AND t.merchant_id IS NOT NULL
      AND d.merchant_id = t.merchant_id
      AND ((NOT v_directional_matching) OR t.tx_is_out)) AS c3,
    -- customer ns + currency (in)
    (SELECT COUNT(*) FROM transactions_scope t JOIN document_scope d
       ON d.company_id = t.company_id::uuid
      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
      AND ((NOT v_directional_matching) OR t.tx_is_in)) AS c4,
    -- merchant ns + currency (out)
    (SELECT COUNT(*) FROM transactions_scope t JOIN document_scope d
       ON d.company_id = t.company_id::uuid
      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
      AND ((NOT v_directional_matching) OR t.tx_is_out)) AS c5,
    -- iban + currency (out)
    (SELECT COUNT(*) FROM transactions_scope t JOIN document_scope d
       ON d.company_id = t.company_id::uuid
      AND d.currency = t.currency
      AND d.doc_iban_ns IS NOT NULL AND t.tx_iban_ns IS NOT NULL
      AND d.doc_iban_ns = t.tx_iban_ns
      AND ((NOT v_directional_matching) OR t.tx_is_out)) AS c6,
    -- amount tol + currency
    (SELECT COUNT(*) FROM transactions_scope t JOIN document_scope d
       ON d.company_id = t.company_id::uuid
      AND d.currency = t.currency
      AND t.effective_amount IS NOT NULL
      AND d.total            IS NOT NULL
      AND d.total BETWEEN (t.effective_amount * (1 - v_amount_tolerance_fraction))
                       AND (t.effective_amount * (1 + v_amount_tolerance_fraction))) AS c7
  INTO v_seed_invoice_cnt, v_seed_order_cnt, v_seed_merchantid_cnt, v_seed_customer_ns_cnt,
       v_seed_merchant_ns_cnt, v_seed_iban_cnt, v_seed_amount_cnt;

  v_t_now := clock_timestamp();
  v_timings := v_timings || jsonb_build_object(
    'stage','seed_counts','ms', FLOOR(EXTRACT(EPOCH FROM (v_t_now - v_t_prev))*1000)::bigint,
    'seeds', jsonb_build_object(
      'invoice',     v_seed_invoice_cnt,
      'order',       v_seed_order_cnt,
      'merchant_id', v_seed_merchantid_cnt,
      'customer_ns', v_seed_customer_ns_cnt,
      'merchant_ns', v_seed_merchant_ns_cnt,
      'iban',        v_seed_iban_cnt,
      'amount_tol',  v_seed_amount_cnt
    )
  );
  v_t_prev := v_t_now;

  ---------------------------------------------------------------------------
  -- MAIN PIPELINE (same logic as 1.9f; accuracy-preserving; no temp tables)
  ---------------------------------------------------------------------------
  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,
      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,
      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,
      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_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '') AS tx_iban_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
  ),

  candidate_pairs AS (
    SELECT t.transaction_id, d1.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      -- 1) Exact invoice match
      SELECT d.document_id
      FROM document_scope d
      WHERE 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

      UNION ALL

      -- 2) Exact order match
      SELECT d.document_id
      FROM document_scope d
      WHERE 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

      UNION ALL

      -- 3) Merchant ID + currency (directional: out)
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.merchant_id IS NOT NULL
        AND t.merchant_id IS NOT NULL
        AND d.merchant_id = t.merchant_id
        AND ((NOT v_directional_matching) OR t.tx_is_out)

      UNION ALL

      -- 4) Customer NS + currency (directional: in)
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        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
        AND ((NOT v_directional_matching) OR t.tx_is_in)

      UNION ALL

      -- 5) Merchant NS + currency (directional: out)
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        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
        AND ((NOT v_directional_matching) OR t.tx_is_out)

      UNION ALL

      -- 6) IBAN + currency (directional: out)
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.doc_iban_ns IS NOT NULL
        AND t.tx_iban_ns  IS NOT NULL
        AND d.doc_iban_ns = t.tx_iban_ns
        AND ((NOT v_directional_matching) OR t.tx_is_out)

      UNION ALL

      -- 7) Amount within tolerance (sargable) + currency
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND t.effective_amount IS NOT NULL
        AND d.total            IS NOT NULL
        AND d.total BETWEEN (t.effective_amount * (1 - v_amount_tolerance_fraction))
                         AND (t.effective_amount * (1 + v_amount_tolerance_fraction))
    ) d1 ON TRUE
  ),

  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_ns,     d.doc_iban_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,

      (
        (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 (
             ((NOT v_directional_matching) OR a.tx_is_out)
             AND 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 (
             ((NOT v_directional_matching) OR a.tx_is_out)
             AND a.tx_iban_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
             AND a.tx_iban_ns = a.doc_iban_ns
           )
      ) AS merchant_match,

      (
        ((NOT v_directional_matching) OR 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)
      ) AS customer_match,

      -- date hard cap for scoring only
      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
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.doc_dedup_key,

      e.invoice_number_match,
      e.order_number_match,
      e.date_within_cap,
      e.currency_match,
      e.merchant_match,
      e.customer_match,

      e.transaction_effective_amount,
      e.document_total,
      e.transaction_date_unix,
      e.document_date_unix,

      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,

      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * v_amount_tolerance_fraction
        THEN TRUE
        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,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN GREATEST(
            0,
            LEAST(
              w.amount_weight,
              w.amount_weight * (1 - (
                ABS(m.transaction_effective_amount - m.document_total)
                / NULLIF(m.document_total * v_amount_tolerance_fraction, 0)
              ))
            )
          )
          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_match,
      m.order_number_match    AS order_match,

      CASE
        WHEN m.date_within_cap AND m.transaction_date_unix IS NOT NULL AND m.document_date_unix IS NOT NULL THEN
          (
            CASE
              WHEN ABS(m.transaction_date_unix - m.document_date_unix) <= (v_date_full_weight_grace_days * 86400)
                THEN w.date_weight
              ELSE
                w.date_weight * GREATEST(
                  0,
                  1 - (
                    ((ABS(m.transaction_date_unix - m.document_date_unix) / 86400.0)
                      - v_date_full_weight_grace_days)::numeric
                    / NULLIF((v_config_date_window_days - v_date_full_weight_grace_days)::numeric, 0)
                  )
                )
            END
          )
        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_confidence AS (
    SELECT
      s.transaction_id,
      s.document_id,
      s.document_company_id,
      s.doc_dedup_key,
      s.transaction_date_unix,
      s.document_date_unix,
      s.amount_direction_ok,

      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,

      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_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 v_weights_total_possible > 0 THEN
        ROUND(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                / v_weights_total_possible) * 100.0, 4)
      ELSE 0 END AS confidence_percent,

      s.amount_match,
      s.invoice_match,
      s.order_match,
      s.date_match,
      s.currency_match,
      s.merchant_match,
      s.customer_match,

      s.date_weight,
      s.invoice_weight,
      s.order_weight,
      s.merchant_weight,
      s.amount_weight,
      s.currency_weight,
      s.customer_weight,

      s.date_score,
      s.invoice_score,
      s.order_score,
      s.merchant_score,
      s.amount_score,
      s.currency_score,
      s.customer_score,

      s.transaction_effective_amount,
      s.document_total
    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.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_match, d.order_match, d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        d.transaction_effective_amount, d.document_total,
        d.transaction_date_unix, d.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
      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
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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,
      r.confidence,
      d.total_numerator,
      v_weights_total_possible,
      d.date_match,
      d.invoice_match,
      d.order_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 final_candidates d ON d.transaction_id = r.transaction_id AND d.document_id = r.document_id
    RETURNING 1
  ),

  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM candidate_pairs)          AS candidate_pairs_count,
      (SELECT COUNT(*) FROM distinct_pairs)           AS distinct_pairs_count,
      (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
    candidate_pairs_count,
    distinct_pairs_count,
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count
  INTO
    v_candidate_pairs_cnt,
    v_distinct_pairs_cnt,
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count
  FROM final_counts;

  v_t_now := clock_timestamp();
  v_timings := v_timings || jsonb_build_object('stage','pipeline_total','ms', FLOOR(EXTRACT(EPOCH FROM (v_t_now - v_t_prev))*1000)::bigint);
  v_t_prev := v_t_now;

  -- 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,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'candidate_pairs', v_candidate_pairs_cnt,
      'distinct_pairs',  v_distinct_pairs_cnt,
      'doc_scope',       v_doc_scope_count,
      'tx_scope',        v_tx_scope_count,
      'seed_counts',     jsonb_build_object(
                           'invoice',     v_seed_invoice_cnt,
                           'order',       v_seed_order_cnt,
                           'merchant_id', v_seed_merchantid_cnt,
                           'customer_ns', v_seed_customer_ns_cnt,
                           'merchant_ns', v_seed_merchant_ns_cnt,
                           'iban',        v_seed_iban_cnt,
                           'amount_tol',  v_seed_amount_cnt
                         ),
      'timings', v_timings
    )
  );

  RETURN jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'candidate_pairs',   v_candidate_pairs_cnt,
    'distinct_pairs',    v_distinct_pairs_cnt,
    'doc_scope',         v_doc_scope_count,
    'tx_scope',          v_tx_scope_count,
    'seeds',             jsonb_build_object(
                           'invoice',     v_seed_invoice_cnt,
                           'order',       v_seed_order_cnt,
                           'merchant_id', v_seed_merchantid_cnt,
                           'customer_ns', v_seed_customer_ns_cnt,
                           'merchant_ns', v_seed_merchant_ns_cnt,
                           'iban',        v_seed_iban_cnt,
                           'amount_tol',  v_seed_amount_cnt
                         ),
    'timings',           v_timings
  );

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,
      'doc_scope', v_doc_scope_count,
      'tx_scope',  v_tx_scope_count,
      'seed_counts', jsonb_build_object(
                       'invoice',     v_seed_invoice_cnt,
                       'order',       v_seed_order_cnt,
                       'merchant_id', v_seed_merchantid_cnt,
                       'customer_ns', v_seed_customer_ns_cnt,
                       'merchant_ns', v_seed_merchant_ns_cnt,
                       'iban',        v_seed_iban_cnt,
                       'amount_tol',  v_seed_amount_cnt
                     ),
      'timings',  v_timings
    )
  );

  RETURN jsonb_build_object(
    'msg','error','time_ms',v_elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',v_err_msg,'sqlstate',v_err_state,
    'doc_scope', v_doc_scope_count, 'tx_scope', v_tx_scope_count,
    'seeds', jsonb_build_object(
               'invoice',     v_seed_invoice_cnt,
               'order',       v_seed_order_cnt,
               'merchant_id', v_seed_merchantid_cnt,
               'customer_ns', v_seed_customer_ns_cnt,
               'merchant_ns', v_seed_merchant_ns_cnt,
               'iban',        v_seed_iban_cnt,
               'amount_tol',  v_seed_amount_cnt
             ),
    'timings',v_timings
  );
END;
$$;

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

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

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

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

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

  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; -- full date weight up to 30 days, then decay to 0 at hard cap
  v_seed_cap_per_match_type   int := 10;   -- reserved (not used for per-tx cap now)
  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;

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

  -- matching options
  v_directional_matching boolean := FALSE;

  -- errors
  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;
BEGIN
  -- load 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,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  -- derive 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;

  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;

  -- resolve 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,
        'seed_cap_per_criterion', v_seed_cap_per_match_type,
        'directional_matching', v_directional_matching
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  -- pipeline
  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,
      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,
      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,
      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_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '') AS tx_iban_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
  ),

  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE 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)
          OR (d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm)

          OR ( ((NOT v_directional_matching) OR (t.tx_is_out)) AND d.currency = t.currency AND d.merchant_id   = t.merchant_id )
          OR ( ((NOT v_directional_matching) OR (t.tx_is_in )) AND d.currency = t.currency AND d.customer_name = t.name_consumer )

          OR ( ((NOT v_directional_matching) OR (t.tx_is_out)) 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.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))

          OR ( ((NOT v_directional_matching) OR (t.tx_is_in)) 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.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))

          OR ( ((NOT v_directional_matching) OR (t.tx_is_out)) 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 t.tx_iban_ns IS NOT NULL AND d.doc_iban_ns IS NOT NULL
               AND t.tx_iban_ns = d.doc_iban_ns)

          OR (
            d.currency = t.currency
            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 (v_amount_tolerance_fraction > 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 = t.credited
                 OR (v_amount_tolerance_fraction > 0
                     AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                   AND (t.credited / (1 - v_amount_tolerance_fraction)))
                 )
              )
            )
          )
        )
      ORDER BY
        CASE
          WHEN d.invoice_number_norm IS NOT NULL AND t.invoice_nr_norm IS NOT NULL AND d.invoice_number_norm = t.invoice_nr_norm THEN 0
          WHEN d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm   THEN 1
          WHEN d.currency = t.currency AND (d.total = t.debited OR d.total = t.credited) THEN 2
          ELSE 3
        END,
        ABS(COALESCE(d.document_date_unix,0) - t.completed_date) ASC,
        d.document_id
    ) d ON TRUE
  ),

  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_ns,     d.doc_iban_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,

      (
        ((NOT v_directional_matching) OR 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_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
              AND a.tx_iban_ns = a.doc_iban_ns)
        )
      ) AS merchant_match,

      (
        ((NOT v_directional_matching) OR 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)
      ) 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
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.doc_dedup_key,

      e.invoice_number_match,
      e.order_number_match,
      e.date_within_cap,
      e.currency_match,
      e.merchant_match,
      e.customer_match,

      e.tx_is_in, e.tx_is_out,

      e.transaction_effective_amount,
      e.document_total,
      e.transaction_date_unix,
      e.document_date_unix,

      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,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.amount_weight,
                w.amount_weight * (1 - (
                  ABS(m.transaction_effective_amount - m.document_total)
                  / NULLIF(m.document_total * v_amount_tolerance_fraction, 0)
                ))
              )
            )
          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_match,
      m.order_number_match    AS order_match,

      CASE
        WHEN m.date_within_cap AND m.transaction_date_unix IS NOT NULL AND m.document_date_unix IS NOT NULL THEN
          (
            CASE
              WHEN ABS(m.transaction_date_unix - m.document_date_unix) <= (v_date_full_weight_grace_days * 86400)
                THEN w.date_weight
              ELSE
                w.date_weight * GREATEST(
                  0,
                  1 - (
                    ((ABS(m.transaction_date_unix - m.document_date_unix) / 86400.0)
                      - v_date_full_weight_grace_days)::numeric
                    / NULLIF((v_config_date_window_days - v_date_full_weight_grace_days)::numeric, 0)
                  )
                )
            END
          )
        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_confidence AS (
    SELECT
      s.*,
      (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_out THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  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 + s.merchant_score + s.customer_score) AS total_numerator,

      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_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 (NOT v_directional_matching) OR s.tx_is_out THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  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 + s.merchant_score + s.customer_score)
                / (
                  s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
                  + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_out THEN s.merchant_weight ELSE 0 END)
                  + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  THEN s.customer_weight ELSE 0 END)
                )) * 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.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_match, d.order_match, d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,

        -- FIX: source from attributes
        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
      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
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_match,
      d.order_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
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count
  INTO
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count
  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,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count
  );

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,'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_f(unknown, unknown) owner to postgres;

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

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

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

create or replace function athena_reconcile_general_v1_9_g(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.9g';

  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;
  v_seed_cap_per_match_type   int := 10;   -- reserved
  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;

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

  -- matching options
  v_directional_matching boolean := TRUE;  -- works both ON and OFF

  -- errors
  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;
BEGIN
  -- load 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,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  -- derive 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;

  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;

  -- resolve 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,
        'seed_cap_per_criterion', v_seed_cap_per_match_type,
        'directional_matching', v_directional_matching
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  -- pipeline
  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,
      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,
      -- IBAN normalization with O->0
      NULLIF(public.norm_iban(replace(UPPER(d.merchant_info::jsonb->>'iban'),'O','0')), '') AS doc_iban_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,
      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,

      -- IBANs (both sides), normalize with O->0
      NULLIF(public.norm_iban(replace(UPPER(COALESCE(t.iban_counterparty, t.iban,'')),'O','0')), '') AS tx_iban_ctr_ns,
      NULLIF(public.norm_iban(replace(UPPER(COALESCE(t.iban_beneficiary,  t.iban,'')),'O','0')), '') AS tx_iban_ben_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
  ),

  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          -- strong IDs
          (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 (d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm)

          -- exact party IDs by FKs (directionally gated)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_out)) AND d.currency = t.currency AND d.merchant_id   = t.merchant_id )
          OR ( ((NOT v_directional_matching) OR (t.tx_is_in )) AND d.currency = t.currency AND d.customer_name = t.name_consumer )

          -- NAME merchant path (needs date window AND amount agreement)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_out))
               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.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
                      OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.debited  / (1 + v_amount_tolerance_fraction))
                                                                                   AND (t.debited  / (1 - v_amount_tolerance_fraction)) )))
                 OR (t.credited IS NOT NULL AND t.credited > 0 AND ( d.total = t.credited
                      OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                                                                   AND (t.credited / (1 - v_amount_tolerance_fraction)) )))
               )
             )

          -- NAME customer path (needs date window AND amount agreement)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_in))
               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.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.debited  IS NOT NULL AND t.debited  > 0 AND ( d.total = t.debited
                      OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.debited  / (1 + v_amount_tolerance_fraction))
                                                                                   AND (t.debited  / (1 - v_amount_tolerance_fraction)) )))
                 OR (t.credited IS NOT NULL AND t.credited > 0 AND ( d.total = t.credited
                      OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                                                                   AND (t.credited / (1 - v_amount_tolerance_fraction)) )))
               )
             )

          -- IBAN paths (direction-aware) + guardrail (amount or id)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_out))
               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 t.tx_iban_ctr_ns IS NOT NULL AND d.doc_iban_ns IS NOT NULL
               AND t.tx_iban_ctr_ns = d.doc_iban_ns
               AND (
                     (t.debited  IS NOT NULL AND t.debited  > 0 AND ( d.total = t.debited
                        OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.debited  / (1 + v_amount_tolerance_fraction))
                                                                                     AND (t.debited  / (1 - v_amount_tolerance_fraction)) )))
                  OR (t.credited IS NOT NULL AND t.credited > 0 AND ( d.total = t.credited
                        OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                                                                     AND (t.credited / (1 - v_amount_tolerance_fraction)) )))
                  OR (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 (d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm)
               )
             )
          OR ( ((NOT v_directional_matching) OR (t.tx_is_in))
               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 t.tx_iban_ben_ns IS NOT NULL AND d.doc_iban_ns IS NOT NULL
               AND t.tx_iban_ben_ns = d.doc_iban_ns
               AND (
                     (t.debited  IS NOT NULL AND t.debited  > 0 AND ( d.total = t.debited
                        OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.debited  / (1 + v_amount_tolerance_fraction))
                                                                                     AND (t.debited  / (1 - v_amount_tolerance_fraction)) )))
                  OR (t.credited IS NOT NULL AND t.credited > 0 AND ( d.total = t.credited
                        OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                                                                     AND (t.credited / (1 - v_amount_tolerance_fraction)) )))
                  OR (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 (d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm)
               )
             )

          -- amount equality / tolerance (direction-aware)
          OR (
            d.currency = t.currency
            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 (v_amount_tolerance_fraction > 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 = t.credited
                 OR (v_amount_tolerance_fraction > 0
                     AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                   AND (t.credited / (1 - v_amount_tolerance_fraction)))
                 )
              )
            )
          )
        )
      ORDER BY
        CASE
          WHEN d.invoice_number_norm IS NOT NULL AND t.invoice_nr_norm IS NOT NULL AND d.invoice_number_norm = t.invoice_nr_norm THEN 0
          WHEN d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm   THEN 1
          WHEN d.currency = t.currency AND (d.total = t.debited OR d.total = t.credited) THEN 2
          ELSE 3
        END,
        ABS(COALESCE(d.document_date_unix,0) - t.completed_date) ASC,
        d.document_id
    ) d ON TRUE
  ),

  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_ctr_ns, t.tx_iban_ben_ns, d.doc_iban_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,

      (
        (
          ((NOT v_directional_matching) OR 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_ctr_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
                AND a.tx_iban_ctr_ns = a.doc_iban_ns)
          )
        )
        OR
        (
          ((NOT v_directional_matching) OR a.tx_is_in) AND
          (a.tx_iban_ben_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
           AND a.tx_iban_ben_ns = a.doc_iban_ns)
        )
      ) AS merchant_match,

      (
        ((NOT v_directional_matching) OR 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)
      ) 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
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.doc_dedup_key,

      e.invoice_number_match,
      e.order_number_match,
      e.date_within_cap,
      e.currency_match,
      e.merchant_match,
      e.customer_match,

      e.tx_is_in, e.tx_is_out,

      e.transaction_effective_amount,
      e.document_total,
      e.transaction_date_unix,
      e.document_date_unix,

      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,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.amount_weight,
                w.amount_weight * (1 - (
                  ABS(m.transaction_effective_amount - m.document_total)
                  / NULLIF(m.document_total * v_amount_tolerance_fraction, 0)
                ))
              )
            )
          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_match,
      m.order_number_match    AS order_match,

      CASE
        WHEN m.date_within_cap AND m.transaction_date_unix IS NOT NULL AND m.document_date_unix IS NOT NULL THEN
          (
            CASE
              WHEN ABS(m.transaction_date_unix - m.document_date_unix) <= (v_date_full_weight_grace_days * 86400)
                THEN w.date_weight
              ELSE
                w.date_weight * GREATEST(
                  0,
                  1 - (
                    ((ABS(m.transaction_date_unix - m.document_date_unix) / 86400.0)
                      - v_date_full_weight_grace_days)::numeric
                    / NULLIF((v_config_date_window_days - v_date_full_weight_grace_days)::numeric, 0)
                  )
                )
            END
          )
        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_confidence AS (
    SELECT
      s.*,
      (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_out THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  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 + s.merchant_score + s.customer_score) AS total_numerator,

      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_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 (NOT v_directional_matching) OR s.tx_is_out THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  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 + s.merchant_score + s.customer_score)
                / (
                  s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
                  + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_out THEN s.merchant_weight ELSE 0 END)
                  + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  THEN s.customer_weight ELSE 0 END)
                )) * 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.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_match, d.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
      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
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_match,
      d.order_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
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count
  INTO
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count
  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,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count
  );

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,'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_g(unknown, unknown) owner to postgres;

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

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

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

create or replace function athena_reconcile_general_v1_9_h(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.9h';

  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;
  v_seed_cap_per_match_type   int := 10;   -- reserved
  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;

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

  -- matching options
  v_directional_matching boolean := FALSE;  -- works ON or OFF

  -- errors
  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;
BEGIN
  -- load 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,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  -- derive 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;

  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;

  -- resolve 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,
        'seed_cap_per_criterion', v_seed_cap_per_match_type,
        'directional_matching', v_directional_matching
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  -- pipeline
  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,
      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,
      -- IBAN normalization with O->0
      NULLIF(public.norm_iban(replace(UPPER(d.merchant_info::jsonb->>'iban'),'O','0')), '') AS doc_iban_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,
      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,

      -- IBANs (both sides), normalize with O->0
      NULLIF(public.norm_iban(replace(UPPER(COALESCE(t.iban_counterparty, t.iban,'')),'O','0')), '') AS tx_iban_ctr_ns,
      NULLIF(public.norm_iban(replace(UPPER(COALESCE(t.iban_beneficiary,  t.iban,'')),'O','0')), '') AS tx_iban_ben_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
  ),

  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          -- strong IDs
          (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 (d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm)

          -- exact party IDs by FKs (directionally gated)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_out)) AND d.currency = t.currency AND d.merchant_id   = t.merchant_id )
          OR ( ((NOT v_directional_matching) OR (t.tx_is_in )) AND d.currency = t.currency AND d.customer_name = t.name_consumer )

          -- NAME merchant path (needs date window AND amount agreement)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_out))
               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.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
                      OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.debited  / (1 + v_amount_tolerance_fraction))
                                                                                   AND (t.debited  / (1 - v_amount_tolerance_fraction)) )))
                 OR (t.credited IS NOT NULL AND t.credited > 0 AND ( d.total = t.credited
                      OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                                                                   AND (t.credited / (1 - v_amount_tolerance_fraction)) )))
               )
             )

          -- NAME customer path (needs date window AND amount agreement)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_in))
               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.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.debited  IS NOT NULL AND t.debited  > 0 AND ( d.total = t.debited
                      OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.debited  / (1 + v_amount_tolerance_fraction))
                                                                                   AND (t.debited  / (1 - v_amount_tolerance_fraction)) )))
                 OR (t.credited IS NOT NULL AND t.credited > 0 AND ( d.total = t.credited
                      OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                                                                   AND (t.credited / (1 - v_amount_tolerance_fraction)) )))
               )
             )

          -- IBAN paths (direction-aware) + guardrail (amount or id)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_out))
               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 t.tx_iban_ctr_ns IS NOT NULL AND d.doc_iban_ns IS NOT NULL
               AND t.tx_iban_ctr_ns = d.doc_iban_ns
               AND (
                     (t.debited  IS NOT NULL AND t.debited  > 0 AND ( d.total = t.debited
                        OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.debited  / (1 + v_amount_tolerance_fraction))
                                                                                     AND (t.debited  / (1 - v_amount_tolerance_fraction)) )))
                  OR (t.credited IS NOT NULL AND t.credited > 0 AND ( d.total = t.credited
                        OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                                                                     AND (t.credited / (1 - v_amount_tolerance_fraction)) )))
                  OR (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 (d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm)
               )
             )
          OR ( ((NOT v_directional_matching) OR (t.tx_is_in))
               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 t.tx_iban_ben_ns IS NOT NULL AND d.doc_iban_ns IS NOT NULL
               AND t.tx_iban_ben_ns = d.doc_iban_ns
               AND (
                     (t.debited  IS NOT NULL AND t.debited  > 0 AND ( d.total = t.debited
                        OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.debited  / (1 + v_amount_tolerance_fraction))
                                                                                     AND (t.debited  / (1 - v_amount_tolerance_fraction)) )))
                  OR (t.credited IS NOT NULL AND t.credited > 0 AND ( d.total = t.credited
                        OR (v_amount_tolerance_fraction > 0 AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                                                                     AND (t.credited / (1 - v_amount_tolerance_fraction)) )))
                  OR (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 (d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm)
               )
             )

          -- amount equality / tolerance (direction-aware)
          OR (
            d.currency = t.currency
            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 (v_amount_tolerance_fraction > 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 = t.credited
                 OR (v_amount_tolerance_fraction > 0
                     AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                   AND (t.credited / (1 - v_amount_tolerance_fraction)))
                 )
              )
            )
          )
        )
      ORDER BY
        CASE
          WHEN d.invoice_number_norm IS NOT NULL AND t.invoice_nr_norm IS NOT NULL AND d.invoice_number_norm = t.invoice_nr_norm THEN 0
          WHEN d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm   THEN 1
          WHEN d.currency = t.currency AND (d.total = t.debited OR d.total = t.credited) THEN 2
          ELSE 3
        END,
        ABS(COALESCE(d.document_date_unix,0) - t.completed_date) ASC,
        d.document_id
    ) d ON TRUE
  ),

  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_ctr_ns, t.tx_iban_ben_ns, d.doc_iban_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,

      (
        (
          ((NOT v_directional_matching) OR 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_ctr_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
                AND a.tx_iban_ctr_ns = a.doc_iban_ns)
          )
        )
        OR
        (
          ((NOT v_directional_matching) OR a.tx_is_in) AND
          (a.tx_iban_ben_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
           AND a.tx_iban_ben_ns = a.doc_iban_ns)
        )
      ) AS merchant_match,

      (
        ((NOT v_directional_matching) OR 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)
      ) 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
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.doc_dedup_key,

      e.invoice_number_match,
      e.order_number_match,
      e.date_within_cap,
      e.currency_match,
      e.merchant_match,
      e.customer_match,

      e.tx_is_in, e.tx_is_out,

      e.transaction_effective_amount,
      e.document_total,
      e.transaction_date_unix,
      e.document_date_unix,

      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,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.amount_weight,
                w.amount_weight * (1 - (
                  ABS(m.transaction_effective_amount - m.document_total)
                  / NULLIF(m.document_total * v_amount_tolerance_fraction, 0)
                ))
              )
            )
          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_match,
      m.order_number_match    AS order_match,

      CASE
        WHEN m.date_within_cap AND m.transaction_date_unix IS NOT NULL AND m.document_date_unix IS NOT NULL THEN
          (
            CASE
              WHEN ABS(m.transaction_date_unix - m.document_date_unix) <= (v_date_full_weight_grace_days * 86400)
                THEN w.date_weight
              ELSE
                w.date_weight * GREATEST(
                  0,
                  1 - (
                    ((ABS(m.transaction_date_unix - m.document_date_unix) / 86400.0)
                      - v_date_full_weight_grace_days)::numeric
                    / NULLIF((v_config_date_window_days - v_date_full_weight_grace_days)::numeric, 0)
                  )
                )
            END
          )
        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_confidence AS (
    SELECT
      s.*,

      -- apply directional gating consistently to denom, numerator, and signal count
      (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_out THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  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 (NOT v_directional_matching) OR s.tx_is_out THEN s.merchant_score ELSE 0 END)
        + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  THEN s.customer_score ELSE 0 END)
      ) AS total_numerator,

      (
        COALESCE(s.amount_match::int,0)
      + COALESCE(s.invoice_match::int,0)
      + COALESCE(s.order_match::int,0)
      + COALESCE(s.date_match::int,0)
      + COALESCE(s.currency_match::int,0)
      + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_out THEN COALESCE(s.merchant_match::int,0) ELSE 0 END)
      + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  THEN COALESCE(s.customer_match::int,0) ELSE 0 END)
      ) AS signals_matched_count,

      CASE WHEN (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_out THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  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 (NOT v_directional_matching) OR s.tx_is_out THEN s.merchant_score ELSE 0 END)
          + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  THEN s.customer_score ELSE 0 END)
        ) / (
          s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
          + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_out THEN s.merchant_weight ELSE 0 END)
          + (CASE WHEN (NOT v_directional_matching) OR s.tx_is_in  THEN s.customer_weight ELSE 0 END)
        )) * 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.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_match, d.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_ben_ns, a.tx_iban_ctr_ns, a.doc_iban_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,
        -- IBANs
        'tx_iban_beneficiary',  e.tx_iban_ben_ns,
        'tx_iban_counterparty', e.tx_iban_ctr_ns,
        'doc_iban',             e.doc_iban_ns,
        -- Direction
        'tx_is_in',  e.tx_is_in,
        'tx_is_out', e.tx_is_out,
        'direction', CASE
                      WHEN e.tx_is_in  AND NOT e.tx_is_out THEN 'incoming'
                      WHEN e.tx_is_out AND NOT e.tx_is_in  THEN 'outgoing'
                      WHEN e.tx_is_in  AND e.tx_is_out     THEN 'both'
                      ELSE 'unknown'
                    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, 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,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_match,
      d.order_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
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count
  INTO
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count
  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,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count
  );

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,'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_h(unknown, unknown) owner to postgres;

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

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

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

create or replace function athena_reconcile_general_v1_9_i(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  ---------------------------------------------------------------------------------------------------
  -- Welcome to the frien engine floris
  -- no this is not the end product of reconciliation
  -- this is simply a stable version that deals with aurora's bed shitting capabilities
  -- works with aurora OCR Commit 7216897 (10/09/25)
  -- pass a reconciliation_config_id and change the config (confidence, log activity, date window days) - the rest arent implimented in this version
  -- 
  -- v1.9i — fix alias bug (invoice_match), include *_customer_ns in scoring for applicability,
  -- keep directional denominator logic + IBAN ‘O’→‘0’, and rich summary.
  -- Requires helpers: norm_code(), norm_core_ns(), norm_text_ns(), norm_iban()
  ----------------------------------------------------------------------------------------------------
  v_status_message text := 'success';
  v_engine_version text := '1.9i';

  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;
  v_seed_cap_per_match_type   int := 10;    -- reserved (not used for nowsies)
  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;

  -- 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;   -- set TRUE for directional, FALSE for symmetric (No weight bias denom)
  -------------------------------------------------------------------------------------------------------------------
  -------------------------------------------------------------------------------------------------------------------
  -- errors
  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;
BEGIN
  -- load 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,
                              '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;

  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;

  -- resolve 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;

  -- optional 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,
        'seed_cap_per_criterion', v_seed_cap_per_match_type,
        'directional_matching', v_directional_matching
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  -- pipeline
  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,
      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(REPLACE(COALESCE(d.merchant_info::jsonb->>'iban',''), 'O','0')), '') AS doc_iban_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,
      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,

      -- IBANs (normalize and fix 'O'→'0')
      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,

      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
  ),

  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE 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)
          OR (d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm)

          OR ( ((NOT v_directional_matching) OR (t.tx_is_out)) AND d.currency = t.currency AND d.merchant_id   = t.merchant_id )
          OR ( ((NOT v_directional_matching) OR (t.tx_is_in )) AND d.currency = t.currency AND d.customer_name = t.name_consumer )

          -- merchant NS with date window (outgoing)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_out)) 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.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))

          -- customer NS with date window (incoming)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_in)) 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.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))

          -- IBAN: OUTGOING → counterparty vs doc IBAN (merchant side)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_out)) 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 t.tx_iban_counterparty_ns IS NOT NULL AND d.doc_iban_ns IS NOT NULL
               AND t.tx_iban_counterparty_ns = d.doc_iban_ns)

          -- IBAN: INCOMING → beneficiary vs doc IBAN (customer side per requirement)
          OR ( ((NOT v_directional_matching) OR (t.tx_is_in)) 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 t.tx_iban_beneficiary_ns IS NOT NULL AND d.doc_iban_ns IS NOT NULL
               AND t.tx_iban_beneficiary_ns = d.doc_iban_ns)

          -- amount tolerance by direction
          OR (
            d.currency = t.currency
            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 (v_amount_tolerance_fraction > 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 = t.credited
                 OR (v_amount_tolerance_fraction > 0
                     AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction))
                                   AND (t.credited / (1 - v_amount_tolerance_fraction)))
                 )
              )
            )
          )
        )
      ORDER BY
        CASE
          WHEN d.invoice_number_norm IS NOT NULL AND t.invoice_nr_norm IS NOT NULL AND d.invoice_number_norm = t.invoice_nr_norm THEN 0
          WHEN d.order_id_norm    IS NOT NULL AND t.order_id_norm   IS NOT NULL AND d.order_id_norm    = t.order_id_norm   THEN 1
          WHEN d.currency = t.currency AND (d.total = t.debited OR d.total = t.credited) THEN 2
          ELSE 3
        END,
        ABS(COALESCE(d.document_date_unix,0) - t.completed_date) ASC,
        d.document_id
    ) d ON TRUE
  ),

  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,

      -- IBANs
      t.tx_iban_beneficiary_ns,
      t.tx_iban_counterparty_ns,
      d.doc_iban_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,

      -- MERCHANT side (outgoing only when directional)
      (
        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)
            ))
          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.doc_iban_ns IS NOT NULL AND a.tx_iban_counterparty_ns IS NOT NULL
                  AND a.tx_iban_counterparty_ns = a.doc_iban_ns)
            )
        END
      ) AS merchant_match,

      -- CUSTOMER side (incoming only when directional); beneficiary IBAN counts as customer evidence
      (
        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)
            ))
          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)
            )
        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_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_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
            GREATEST(
              0,
              LEAST(
                w.amount_weight,
                w.amount_weight * (1 - (
                  ABS(m.transaction_effective_amount - m.document_total)
                  / NULLIF(m.document_total * v_amount_tolerance_fraction, 0)
                ))
              )
            )
          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.date_within_cap AND m.transaction_date_unix IS NOT NULL AND m.document_date_unix IS NOT NULL THEN
          (
            CASE
              WHEN ABS(m.transaction_date_unix - m.document_date_unix) <= (v_date_full_weight_grace_days * 86400)
                THEN w.date_weight
              ELSE
                w.date_weight * GREATEST(
                  0,
                  1 - (
                    ((ABS(m.transaction_date_unix - m.document_date_unix) / 86400.0)
                      - v_date_full_weight_grace_days)::numeric
                    / NULLIF((v_config_date_window_days - v_date_full_weight_grace_days)::numeric, 0)
                  )
                )
            END
          )
        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_confidence AS (
    SELECT
      s.*,

      -- Applicability flags
      (
        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)
              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)
              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)
            ))
          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)
            )
        END
      ) AS customer_applicable,

      -- dynamic denominator
      (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (CASE WHEN (
              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)
                       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)
                       OR  (s.document_merchant_id IS NOT NULL AND s.transaction_merchant_id IS NOT NULL))
              END
            ) THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN (
              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)))
                   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))
              END
            ) THEN s.customer_weight ELSE 0 END)
      )::numeric AS dynamic_total_weight,

      -- numerator mirror
      (
        s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
        + (CASE WHEN (
              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)
                       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)
                       OR  (s.document_merchant_id IS NOT NULL AND s.transaction_merchant_id IS NOT NULL))
              END
            ) THEN s.merchant_score ELSE 0 END)
        + (CASE WHEN (
              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)))
                   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))
              END
            ) 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 (
              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)
                       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)
                       OR  (s.document_merchant_id IS NOT NULL AND s.transaction_merchant_id IS NOT NULL))
              END
            ) THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN (
              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)))
                   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))
              END
            ) 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 (
                    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)
                             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)
                             OR  (s.document_merchant_id IS NOT NULL AND s.transaction_merchant_id IS NOT NULL))
                    END
                  ) THEN s.merchant_score ELSE 0 END)
              + (CASE WHEN (
                    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)))
                         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))
                    END
                  ) THEN s.customer_score ELSE 0 END)
            ) / (
                s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
              + (CASE WHEN (
                    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)
                             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)
                             OR  (s.document_merchant_id IS NOT NULL AND s.transaction_merchant_id IS NOT NULL))
                    END
                  ) THEN s.merchant_weight ELSE 0 END)
              + (CASE WHEN (
                    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)))
                         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))
                    END
                  ) THEN s.customer_weight ELSE 0 END)
            )) * 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.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,

        -- IBANs + direction for summary
        a.tx_iban_beneficiary_ns, a.tx_iban_counterparty_ns, a.doc_iban_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_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
      )::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
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count
  INTO
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count
  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,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count
  );

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,'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_i(unknown, unknown) owner to postgres;

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

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

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

create or replace function auto_stage_reconciliations_v1(p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE 
  v_rows       integer := 0;
  v_verified   integer := 0;
  v_status     text    := 'ok';
  v_msg        text    := 'success';
  v_start      timestamptz := clock_timestamp();
  v_end        timestamptz;
  v_ms         bigint;
  v_run_id     uuid;

  -- config
  v_auto_reconcile        boolean := FALSE;
  v_auto_reconcile_min    numeric := NULL;   -- as stored
  v_threshold_pct         numeric := NULL;   -- normalized to 0..100

  -- collect inserted ids to evaluate threshold without temp tables
  v_inserted_ids uuid[] := '{}';
  r_rec record;
BEGIN
  -- read config (auto_reconcile + min pct)
  SELECT
    COALESCE(c.auto_reconcile, FALSE),
    c.auto_reconcile_min_pct
  INTO v_auto_reconcile, v_auto_reconcile_min
  FROM public.reconciliations_config c
  WHERE c.reconciliation_config_id = p_reconciliation_config_id;

  -- normalize threshold: treat <=1 as fraction -> percent; leave NULL as NULL (no auto-verify)
  IF v_auto_reconcile_min IS NOT NULL THEN
    IF v_auto_reconcile_min <= 1 THEN
      v_threshold_pct := v_auto_reconcile_min * 100.0;
    ELSE
      v_threshold_pct := v_auto_reconcile_min;
    END IF;
  END IF;

  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => 'auto_stage_reconciliations_v1',
    p_company_id       => NULL,
    p_context          => jsonb_build_object(
      'target_table','reconciliations_staging',
      'auto_reconcile', v_auto_reconcile,
      'auto_reconcile_min_pct', v_threshold_pct
    )
  );

  -- insert and capture inserted ids (no temp tables)
  FOR r_rec IN
    WITH ins AS (
      INSERT INTO public.reconciliations_staging (reconciliation_id, company_id)
      SELECT r.reconciliation_id, r.company_id
      FROM public.reconciliations r
      WHERE NOT EXISTS (
        SELECT 1
        FROM public.reconciliations_staging s
        WHERE s.reconciliation_id = r.reconciliation_id
      )
      RETURNING reconciliation_id, company_id
    )
    SELECT reconciliation_id, company_id FROM ins
  LOOP
    v_rows := v_rows + 1;
    v_inserted_ids := v_inserted_ids || r_rec.reconciliation_id;
  END LOOP;

  -- step log (staging)
  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => 'auto_stage_reconciliations_v1',
    p_action            => 'staged',
    p_status            => 'ok',
    p_target            => 'reconciliations_staging',
    p_message           => NULL,
    p_company_id        => NULL,
    p_transaction_id    => NULL,
    p_document_id       => NULL,
    p_reconciliation_id => NULL,
    p_rows_affected     => v_rows,
    p_confidence        => NULL,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_start)))::bigint),
    p_processing_ms     => NULL,
    p_context           => jsonb_build_object('note','rows inserted into staging')
  );

  -- auto-verify (only if enabled AND we have a threshold AND there are inserted rows)
  IF v_auto_reconcile AND v_threshold_pct IS NOT NULL AND array_length(v_inserted_ids,1) IS NOT NULL THEN
    FOR r_rec IN
      SELECT r.reconciliation_id
      FROM public.reconciliations r
      WHERE r.reconciliation_id = ANY (v_inserted_ids)
        AND r.confidence IS NOT NULL
        AND r.confidence >= v_threshold_pct
    LOOP
      PERFORM public.reconciliations_staging_update_v2(
        p_reconciliation_id => r_rec.reconciliation_id,
        p_verified          => TRUE
      );
      v_verified := v_verified + 1;
    END LOOP;

    -- step log (auto-verify)
    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'auto_stage_reconciliations_v1',
      p_action            => 'auto_verify',
      p_status            => 'ok',
      p_target            => 'reconciliations_staging',
      p_message           => NULL,
      p_company_id        => NULL,
      p_transaction_id    => NULL,
      p_document_id       => NULL,
      p_reconciliation_id => NULL,
      p_rows_affected     => v_verified,
      p_confidence        => NULL,
      p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_start)))::bigint),
      p_processing_ms     => NULL,
      p_context           => jsonb_build_object(
        'auto_reconcile', v_auto_reconcile,
        'auto_reconcile_min_pct', v_threshold_pct
      )
    );
  END IF;

  v_end := clock_timestamp();
  v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::bigint;

  -- end log
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => 'auto_stage_reconciliations_v1',
    p_status            => 'ok',
    p_message           => NULL,
    p_processing_ms     => v_ms,
    p_context           => jsonb_build_object(
      'reconciliations_staged', v_rows,
      'auto_verified',          v_verified,
      'auto_reconcile',         v_auto_reconcile,
      'auto_reconcile_min_pct', v_threshold_pct
    )
  );

  RETURN jsonb_build_object(
    'status',                 v_status,
    'message',                v_msg,
    'processing_time',        v_ms,
    'reconciliations_staged', v_rows,
    'auto_verified',          v_verified,
    'auto_reconcile',         v_auto_reconcile,
    'auto_reconcile_min_pct', v_threshold_pct
  );

EXCEPTION WHEN OTHERS THEN
  v_status := 'error';
  v_msg := SQLERRM;
  v_end := clock_timestamp();
  v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::bigint;

  -- error step
  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => 'auto_stage_reconciliations_v1',
    p_action            => 'staged',
    p_status            => 'error',
    p_target            => 'reconciliations_staging',
    p_message           => v_msg,
    p_company_id        => NULL,
    p_transaction_id    => NULL,
    p_document_id       => NULL,
    p_reconciliation_id => NULL,
    p_rows_affected     => v_rows,
    p_confidence        => NULL,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_start)))::bigint),
    p_processing_ms     => NULL,
    p_context           => jsonb_build_object('hint','see SQL error above')
  );

  -- end log (error)
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => 'auto_stage_reconciliations_v1',
    p_status            => 'error',
    p_message           => v_msg,
    p_processing_ms     => v_ms,
    p_context           => jsonb_build_object(
      'reconciliations_staged', v_rows,
      'auto_verified',          v_verified
    )
  );

  RETURN jsonb_build_object(
    'status','error',
    'message', v_msg,
    'processing_time', v_ms,
    'reconciliations_staged', v_rows,
    'auto_verified', v_verified
  );
END;
$$;

alter function auto_stage_reconciliations_v1(unknown) owner to postgres;

grant execute on function auto_stage_reconciliations_v1(unknown) to anon;

grant execute on function auto_stage_reconciliations_v1(unknown) to authenticated;

grant execute on function auto_stage_reconciliations_v1(unknown) to service_role;

create or replace function reconciliations_unverify_bulk_v1(p_reconciliation_ids uuid[] DEFAULT NULL::uuid[], p_company_id uuid DEFAULT NULL::uuid, p_since timestamp with time zone DEFAULT NULL::timestamp with time zone, p_limit integer DEFAULT NULL::integer, p_publish_event boolean DEFAULT true) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_run_id        uuid;
  v_t0            timestamptz := clock_timestamp();
  v_targets       uuid[];
  v_id            uuid;
  v_ok            int := 0;
  v_err           int := 0;
  v_last_err      text := NULL;
BEGIN
  -- start log
  v_run_id := public.reconciliations_log_start(
    p_function_version => 'reconciliations_unverify_bulk_v1',
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
      'ids_given', p_reconciliation_ids IS NOT NULL,
      'company_id', p_company_id,
      'since', p_since,
      'limit', p_limit
    )
  );

  -- build target list (only currently verified)
  WITH base AS (
    SELECT r.reconciliation_id
    FROM public.reconciliations r
    WHERE r.verified IS TRUE
      AND (p_company_id IS NULL OR r.company_id = p_company_id)
      AND (p_since IS NULL OR r.created_at >= p_since)
      AND (p_reconciliation_ids IS NULL OR r.reconciliation_id = ANY(p_reconciliation_ids))
    ORDER BY r.created_at DESC, r.reconciliation_id
    LIMIT COALESCE(p_limit, 2147483647)
  )
  SELECT COALESCE(array_agg(reconciliation_id), ARRAY[]::uuid[])
  INTO v_targets
  FROM base;

  -- short-circuit if nothing to do
  IF array_length(v_targets,1) IS NULL THEN
    PERFORM public.reconciliations_log_end(
      p_run_id           => v_run_id,
      p_function_version => 'reconciliations_unverify_bulk_v1',
      p_status           => 'ok',
      p_message          => NULL,
      p_processing_ms    => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_context          => jsonb_build_object('count', 0)
    );
    RETURN jsonb_build_object('ok', true, 'unverified', 0, 'skipped', 0, 'errors', 0);
  END IF;

  -- loop and reuse your verified-cascade helper for consistent side effects
  FOREACH v_id IN ARRAY v_targets LOOP
    BEGIN
      PERFORM public.reconciliations_staging_update_v2(
        p_reconciliation_id => v_id,
        p_verified          => FALSE,
        p_publish_event     => p_publish_event
      );
      v_ok := v_ok + 1;
    EXCEPTION WHEN OTHERS THEN
      v_err := v_err + 1;
      v_last_err := SQLERRM;
    END;
  END LOOP;

  -- end log
  PERFORM public.reconciliations_log_end(
    p_run_id           => v_run_id,
    p_function_version => 'reconciliations_unverify_bulk_v1',
    p_status           => CASE WHEN v_err = 0 THEN 'ok' ELSE 'error' END,
    p_message          => v_last_err,
    p_processing_ms    => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_context          => jsonb_build_object(
      'attempted', COALESCE(array_length(v_targets,1),0),
      'unverified', v_ok,
      'errors', v_err
    )
  );

  RETURN jsonb_build_object(
    'ok', (v_err = 0),
    'attempted', COALESCE(array_length(v_targets,1),0),
    'unverified', v_ok,
    'errors', v_err
  );
END;
$$;

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

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

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

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

create or replace function v_avg_confidence(company_id_input uuid) returns jsonb
    stable
    language sql
as
$$
    SELECT to_jsonb(sub)
    FROM (
        SELECT 
            company_id,
            AVG(confidence)::numeric AS avg_confidence
        FROM reconciliations_match_scores
        WHERE company_id = company_id_input
        GROUP BY company_id
    ) sub;
$$;

alter function v_avg_confidence(unknown) owner to postgres;

grant execute on function v_avg_confidence(unknown) to anon;

grant execute on function v_avg_confidence(unknown) to authenticated;

grant execute on function v_avg_confidence(unknown) to service_role;

create or replace function avg_confidence(company_id_input uuid) returns jsonb
    stable
    language sql
as
$$
    SELECT to_jsonb(sub)
    FROM (
        SELECT 
            company_id,
            AVG(confidence)::numeric AS avg_confidence
        FROM reconciliations_match_scores
        WHERE company_id = company_id_input
        GROUP BY company_id
    ) sub;
$$;

alter function avg_confidence(unknown) owner to postgres;

grant execute on function avg_confidence(unknown) to anon;

grant execute on function avg_confidence(unknown) to authenticated;

grant execute on function avg_confidence(unknown) to service_role;

create or replace function athena_reconcile_general_v1_9_j(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.9j';

  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;
  v_cap_precise               int := 5;    -- capped “precise” stages
  v_cap_broad                 int := 10;   -- capped broad stages
  v_cap_window_only           int := 3;    -- last resort cap
  v_cap_iban_exact            int := 5;    -- NEW: cap for IBAN exact per transaction
  v_iban_exact_window_seconds bigint;      -- NEW: optional tighter window for IBAN exact
  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
  -- load 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;

  -- tighter window for IBAN exact: min(global, 14 days)
  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;

  -- resolve 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
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  -- pipeline
  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,
      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(REPLACE(COALESCE(d.merchant_info::jsonb->>'iban',''), 'O','0')), '') AS doc_iban_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,
      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,

      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) ====================== */

  -- 1) Invoice number =
  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
  ),

  -- 2) Order number =
  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
  ),

  -- 3) IBAN = AND exact amount AND tight date window (CAP + ORDER BY ABS(date))
  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 d.doc_iban_ns IS NOT NULL AND t.tx_iban_counterparty_ns IS NOT NULL
        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 = t.tx_iban_counterparty_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 = t.company_id::uuid
        AND ((NOT v_directional_matching) OR t.tx_is_in)
        AND d.currency = t.currency
        AND d.doc_iban_ns IS NOT NULL AND t.tx_iban_beneficiary_ns IS NOT NULL
        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 = t.tx_iban_beneficiary_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
  ),

  -- 4) Merchant_id = AND exact amount AND date window (outgoing)
  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 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
  ),

  -- 5) Merchant NS match AND exact amount AND date window (outgoing)
  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 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
  ),

  -- 6) Customer NS match AND exact amount AND date window (incoming)
  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 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
  ),

  -- 7) IBAN = AND one extra signal AND date window  [capped precise]
  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 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 ( ((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 )
        )
        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)
      LIMIT v_cap_precise
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),

  -- 8) Merchant_id = AND amount tolerance AND date window  [outgoing]
  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 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
  ),

  -- 9) Merchant NS AND amount tolerance AND date window  [outgoing]
  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 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
  ),

  -- 10) Customer NS AND amount tolerance AND date window  [incoming]
  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 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
  ),

  -- 11) IBAN = AND amount tolerance AND date window  [both]
  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 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
            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
            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
  ),

  -- 12) Date window AND exact amount
  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 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
  ),

  -- 13) Date window AND amount tolerance
  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 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
  ),

  -- 14) Date window only
  s14_window_only AS (
