create or replace function postgres_fdw_handler() returns fdw_handler
    strict
    language c
as
$$
begin
-- missing source code
end;
$$;

alter function postgres_fdw_handler() owner to supabase_admin;

grant execute on function postgres_fdw_handler() to postgres;

grant execute on function postgres_fdw_handler() to anon;

grant execute on function postgres_fdw_handler() to authenticated;

grant execute on function postgres_fdw_handler() to service_role;

create or replace function postgres_fdw_validator(text[], oid) returns void
    strict
    language c
as
$$
begin
-- missing source code
end;
$$;

alter function postgres_fdw_validator(text[], oid) owner to supabase_admin;

grant execute on function postgres_fdw_validator(text[], oid) to postgres;

grant execute on function postgres_fdw_validator(text[], oid) to anon;

grant execute on function postgres_fdw_validator(text[], oid) to authenticated;

grant execute on function postgres_fdw_validator(text[], oid) to service_role;

create or replace function postgres_fdw_get_connections(out server_name text, out valid boolean) returns setof record
    strict
    parallel restricted
    language c
as
$$
begin
-- missing source code
end;
$$;

alter function postgres_fdw_get_connections(out text, out boolean) owner to supabase_admin;

grant execute on function postgres_fdw_get_connections(out text, out boolean) to postgres;

grant execute on function postgres_fdw_get_connections(out text, out boolean) to anon;

grant execute on function postgres_fdw_get_connections(out text, out boolean) to authenticated;

grant execute on function postgres_fdw_get_connections(out text, out boolean) to service_role;

create or replace function postgres_fdw_disconnect(text) returns boolean
    strict
    parallel restricted
    language c
as
$$
begin
-- missing source code
end;
$$;

alter function postgres_fdw_disconnect(text) owner to supabase_admin;

grant execute on function postgres_fdw_disconnect(text) to postgres;

grant execute on function postgres_fdw_disconnect(text) to anon;

grant execute on function postgres_fdw_disconnect(text) to authenticated;

grant execute on function postgres_fdw_disconnect(text) to service_role;

create or replace function postgres_fdw_disconnect_all() returns boolean
    strict
    parallel restricted
    language c
as
$$
begin
-- missing source code
end;
$$;

alter function postgres_fdw_disconnect_all() owner to supabase_admin;

grant execute on function postgres_fdw_disconnect_all() to postgres;

grant execute on function postgres_fdw_disconnect_all() to anon;

grant execute on function postgres_fdw_disconnect_all() to authenticated;

grant execute on function postgres_fdw_disconnect_all() to service_role;

create or replace function fetch_sessions()
    returns TABLE(ip text, user_id uuid, refreshed_at timestamp without time zone, created_at timestamp without time zone)
    language plpgsql
as
$$
BEGIN
    RETURN QUERY
    SELECT s.ip, s.user_id, s.refreshed_at, s.created_at
    FROM auth.sessions s;  -- Use table alias 's' for the sessions table
END;
$$;

alter function fetch_sessions() owner to postgres;

grant execute on function fetch_sessions() to anon;

grant execute on function fetch_sessions() to authenticated;

grant execute on function fetch_sessions() to service_role;

create or replace function copy_sessions_to_public() returns void
    language plpgsql
as
$$
BEGIN
    DELETE FROM public.sessions;
    -- Insert data from auth.sessions to public.sessions
    INSERT INTO public.sessions (
        session_id,      -- Mapping auth.sessions.id to public.sessions.session_id
        created_at, 
        user_id, 
        updated_at, 
        factor_id, 
        aal, 
        user_agent, 
        ip, 
        tag, 
        refreshed_at, 
        not_after
    )
    SELECT
        id,  -- Use the id from auth.sessions as session_id in public.sessions
        created_at,
        user_id,
        updated_at,
        NULL,  -- Assuming factor_id is not available in auth.sessions
        NULL,  -- Assuming aal is not available in auth.sessions
        user_agent,  
        ip,
        NULL,  -- Assuming tag is not available in auth.sessions
        refreshed_at,
        NULL   -- Assuming not_after is not available in auth.sessions
    FROM auth.sessions;
END;
$$;

alter function copy_sessions_to_public() owner to postgres;

grant execute on function copy_sessions_to_public() to anon;

grant execute on function copy_sessions_to_public() to authenticated;

grant execute on function copy_sessions_to_public() to service_role;

create or replace function run_sql(query text) returns jsonb
    security definer
    language plpgsql
as
$$
declare
  result jsonb;
begin
  -- wrap the user’s SQL in a sub-select so we can return a single JSON blob
  execute format(
    'select jsonb_agg(t) from (%s) as t',
    query
  ) into result;
  return coalesce(result, '[]'::jsonb); -- ← changed from json to jsonb (or add a comment)
end;
$$;

alter function run_sql(unknown) owner to postgres;

grant execute on function run_sql(unknown) to anon;

grant execute on function run_sql(unknown) to authenticated;

grant execute on function run_sql(unknown) to service_role;

create or replace function pg_schema()
    returns TABLE(table_name text)
    language sql
as
$$
  SELECT table_name
  FROM information_schema.tables
  WHERE table_schema = 'public'
    AND table_type = 'BASE TABLE';
$$;

alter function pg_schema() owner to postgres;

grant execute on function pg_schema() to anon;

grant execute on function pg_schema() to authenticated;

grant execute on function pg_schema() to service_role;

create or replace function execute_sql(sql text) returns void
    security definer
    language plpgsql
as
$$
BEGIN
  EXECUTE sql;
END;
$$;

alter function execute_sql(unknown) owner to postgres;

grant execute on function execute_sql(unknown) to anon;

grant execute on function execute_sql(unknown) to authenticated;

grant execute on function execute_sql(unknown) to service_role;

create or replace function reconcile_now(p_athena_view text DEFAULT 'v_reconciled_totals'::text, p_company_id uuid DEFAULT NULL::uuid) returns integer
    language plpgsql
as
$$
DECLARE
  v_has_unique boolean;
  v_sql text;
  v_rows integer := 0;
BEGIN
  -- Detect if there is a UNIQUE index/constraint on (company_id, transaction_id, document_id)
  SELECT EXISTS (
    SELECT 1
    FROM pg_indexes
    WHERE schemaname = 'public'
      AND tablename  = 'reconciliations'
      AND indexdef ILIKE '%UNIQUE%' 
      AND (
        indexdef ILIKE '%(company_id, transaction_id, document_id)%'
        OR indexdef ILIKE '%(company_id,transaction_id,document_id)%'
      )
  ) INTO v_has_unique;

  IF v_has_unique THEN
    -- Fast path: ON CONFLICT (requires a unique index/constraint)
    v_sql := $f$
      INSERT INTO public.reconciliations (
        athena_view,
        transaction_id,
        document_id,
        company_id
      )
      SELECT
        $1::text                           AS athena_view,
        t.transaction_id,
        d.document_id,
        t.company_id
      FROM public.transactions t
      JOIN public.documents_v2 d
        ON d.total = t.debited
       AND COALESCE(d.currency, t.currency) = t.currency
       AND d.company_id = t.company_id
      WHERE ($2::uuid IS NULL OR t.company_id = $2::uuid)
      ON CONFLICT (company_id, transaction_id, document_id) DO NOTHING
    $f$;

    EXECUTE v_sql USING p_athena_view, p_company_id;
    GET DIAGNOSTICS v_rows = ROW_COUNT;

  ELSE
    -- Safe path: anti-join without requiring schema changes
    v_sql := $f$
      WITH src AS (
        SELECT
          t.transaction_id,
          d.document_id,
          t.company_id
        FROM public.transactions t
        JOIN public.documents_v2 d
          ON d.total = t.debited
         AND COALESCE(d.currency, t.currency) = t.currency
         AND d.company_id = t.company_id
        WHERE ($2::uuid IS NULL OR t.company_id = $2::uuid)
      )
      INSERT INTO public.reconciliations (
        athena_view,
        transaction_id,
        document_id,
        company_id
      )
      SELECT
        $1::text,
        s.transaction_id,
        s.document_id,
        s.company_id
      FROM src s
      WHERE NOT EXISTS (
        SELECT 1
        FROM public.reconciliations r
        WHERE r.company_id    = s.company_id
          AND r.transaction_id = s.transaction_id
          AND r.document_id    = s.document_id
      )
    $f$;

    EXECUTE v_sql USING p_athena_view, p_company_id;
    GET DIAGNOSTICS v_rows = ROW_COUNT;
  END IF;

  RETURN v_rows;
END;
$$;

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

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

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

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

create or replace function list_views()
    returns TABLE(table_schema text, table_name text)
    stable
    language sql
as
$$
  SELECT table_schema, table_name
  FROM information_schema.views
  WHERE table_schema NOT in ('pg_catalog', 'information_schema')
  ORDER BY table_schema, table_name;
$$;

alter function list_views() owner to postgres;

grant execute on function list_views() to anon;

grant execute on function list_views() to authenticated;

grant execute on function list_views() to service_role;

create or replace function list_extensions()
    returns TABLE(ext_name text, ext_comment text, default_version text, installed_version text)
    stable
    language sql
as
$$
  SELECT
    e.name,
    e.comment,
    e.default_version,
    e.installed_version
  FROM pg_available_extensions e
  ORDER BY e.name ASC;
$$;

alter function list_extensions() owner to postgres;

grant execute on function list_extensions() to anon;

grant execute on function list_extensions() to authenticated;

grant execute on function list_extensions() to service_role;

create or replace function schema(p_table_name text, p_schema_name text DEFAULT 'public'::text) returns jsonb
    language plpgsql
as
$$
BEGIN
  RETURN (
    SELECT jsonb_object_agg(
             column_name,
             jsonb_build_object(
               'is_nullable', (is_nullable = 'YES'),
               'type', udt_name
             )
           )
    FROM information_schema.columns
    WHERE table_schema = p_schema_name
      AND table_name = p_table_name
  );
END;
$$;

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

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

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

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

create or replace function sync_sg_companies_to_global() returns void
    language plpgsql
as
$$
begin
  insert into public.global_companies_registry (
    name,
    company_number,
    country_code,
    status,
    establishment_date,
    entity_type,
    description,
    postal_code,
    street,
    house_number,
    city,
    state,
    region,
    last_update
  )
  select
    sc.entity_name as name,
    sc.uen as company_number,
    'SG' as country_code,
    sc.entity_status_description as status,
    sc.registration_incorporation_date as establishment_date,
    sc.entity_type_description as entity_type,
    sc.primary_user_described_activity as description,
    sc.postal_code,
    sc.street_name as street,
    sc.block as house_number,
    sc.building_name as city,
    sc.level_no as state,
    sc.unit_no as region,
    now()::text as last_update
  from public.sg_companies sc
  on conflict (hash) do nothing;
end;
$$;

alter function sync_sg_companies_to_global() owner to postgres;

grant execute on function sync_sg_companies_to_global() to anon;

grant execute on function sync_sg_companies_to_global() to authenticated;

grant execute on function sync_sg_companies_to_global() to service_role;

create or replace function stage_reconciliations() returns jsonb
    language plpgsql
as
$$
DECLARE 
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end TIMESTAMPTZ;
    v_ms BIGINT;
    v_sql TEXT;
BEGIN
    -- Build insert SQL (keep same structure as your v2 reconcile function)
    v_sql := $f$
        INSERT INTO public.reconciliations_staging (reconciliation_id)
        SELECT r.reconciliation_id
        FROM public.reconciliations r
        -- optional: avoid duplicates if staging already has them
        WHERE NOT EXISTS (
            SELECT 1
            FROM public.reconciliations_staging s
            WHERE s.reconciliation_id = r.reconciliation_id
        )
    $f$;

    BEGIN
        EXECUTE v_sql;
        GET DIAGNOSTICS v_rows = ROW_COUNT;
    EXCEPTION
        WHEN OTHERS THEN
            v_status := 'error';
            v_msg := SQLERRM;
    END;

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

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

alter function stage_reconciliations() owner to postgres;

grant execute on function stage_reconciliations() to anon;

grant execute on function stage_reconciliations() to authenticated;

grant execute on function stage_reconciliations() to service_role;

create or replace function athena_reconcile_v2(p_athena_view text DEFAULT 'v_reconciled_totals'::text) returns jsonb
    language plpgsql
as
$$
declare 
    v_has_unique boolean;
    v_sql text;
    v_rows integer := 0;
    v_status text := 'ok';
    v_msg text := 'success';
    v_start timestamptz := clock_timestamp();
    v_end timestamptz;
    v_ms bigint;
begin
    -- ensure pgcrypto exists
    begin
        create extension if not exists pgcrypto;
    exception
        when insufficient_privilege or duplicate_object then null;
    end;

    -- check if unique constraint exists
    select exists (
        select 1
        from pg_constraint c
        join pg_class t on t.oid = c.conrelid and t.relname = 'reconciliations'
        join pg_namespace n on n.oid = t.relnamespace and n.nspname = 'public'
        where c.contype = 'u'
          and (
              select array_agg(attname::text order by attnum)
              from pg_attribute
              where attrelid = t.oid
                and attnum = any (c.conkey)
          ) = array['company_id','transaction_id','document_id']::text[]
    ) into v_has_unique;

    if v_has_unique then
        v_sql := $f$
            with tx as (
                select t.transaction_id,
                       t.company_id,
                       t.currency,
                       nullif(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric as debited_num
                from public.transactions t
                where nullif(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '') is not null
            )
            insert into public.reconciliations (
                athena_view,
                transaction_id,
                document_id,
                company_id
            )
            select $1::text,
                   tx.transaction_id::uuid,
                   d.document_id::uuid,
                   tx.company_id::uuid
            from tx
            join public.documents_v2 d
              on round(d.total::numeric, 2) = round(tx.debited_num, 2)
             and (d.currency is null or upper(d.currency) = upper(tx.currency))
            where tx.company_id::uuid = d.company_id::uuid
            on conflict (company_id, transaction_id, document_id) do nothing
        $f$;
    else
        v_sql := $f$
            with tx as (
                select t.transaction_id,
                       t.company_id,
                       t.currency,
                       nullif(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric as debited_num
                from public.transactions t
                where nullif(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '') is not null
            ),
            src as (
                select tx.transaction_id,
                       d.document_id,
                       tx.company_id
                from tx
                join public.documents_v2 d
                  on round(d.total::numeric, 2) = round(tx.debited_num, 2)
                 and (d.currency is null or upper(d.currency) = upper(tx.currency))
                where tx.company_id::uuid = d.company_id::uuid
            )
            insert into public.reconciliations (
                athena_view,
                transaction_id,
                document_id,
                company_id
            )
            select $1::text,
                   s.transaction_id::uuid,
                   s.document_id::uuid,
                   s.company_id::uuid
            from src s
            where not exists (
                select 1
                from public.reconciliations r
                where r.company_id = s.company_id::uuid
                  and r.transaction_id = s.transaction_id::uuid
                  and r.document_id = s.document_id::uuid
            )
        $f$;
    end if;

    begin
        execute v_sql using p_athena_view;
        get diagnostics v_rows = row_count;
    exception
        when others then
            v_status := 'error';
            v_msg := sqlerrm;
    end;

    v_end := clock_timestamp();
    v_ms := floor(extract(epoch from (v_end - v_start)) * 1000)::bigint;

    return jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
end;
$$;

alter function athena_reconcile_v2(unknown) owner to postgres;

grant execute on function athena_reconcile_v2(unknown) to anon;

grant execute on function athena_reconcile_v2(unknown) to authenticated;

grant execute on function athena_reconcile_v2(unknown) to service_role;

create or replace function fetch_and_cache_link_meta(p_url text) returns bigint
    language plpgsql
as
$$
DECLARE
    resp json;
    title text;
    description text;
    images json := '[]'::json;
    domain text;
    content_type text;
    cache_id bigint;
BEGIN
    -- Fetch URL
    resp := (SELECT content::json
             FROM http_get(p_url, 'application/json', ARRAY[http_header('User-Agent','Mozilla/5.0')]));

    -- Extract domain
    domain := split_part(p_url, '/', 3);

    -- Try naive title parsing (depends on response type)
    title := (regexp_match(resp::text, '<title>(.*?)</title>'))[1];
    description := (regexp_match(resp::text, '<meta name="description" content="(.*?)"'))[1];

    -- Insert into cache table
    INSERT INTO public.link_meta_preview_cache (
        url, title, description, content_type, domain
    )
    VALUES (
        p_url, title, description, content_type, domain
    )
    RETURNING id INTO cache_id;

    RETURN cache_id;
EXCEPTION
    WHEN others THEN
        RAISE NOTICE 'Error fetching/parsing %: %', p_url, SQLERRM;
        RETURN NULL;
END;
$$;

alter function fetch_and_cache_link_meta(unknown) owner to postgres;

grant execute on function fetch_and_cache_link_meta(unknown) to anon;

grant execute on function fetch_and_cache_link_meta(unknown) to authenticated;

grant execute on function fetch_and_cache_link_meta(unknown) to service_role;

create or replace function athena_reconcile_v5(p_athena_view text DEFAULT 'v_reconciled_totals'::text) returns jsonb
    language plpgsql
as
$$
DECLARE 
    v_has_unique BOOLEAN;
    v_sql TEXT;               -- holds the INSERT body (without RETURNING wrapper)
    v_rows INTEGER := 0;      -- rows inserted
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end TIMESTAMPTZ;
    v_ms BIGINT;
BEGIN 
    -- ensure pgcrypto exists (no-op if lacks privilege)
    BEGIN 
        CREATE EXTENSION IF NOT EXISTS pgcrypto;
    EXCEPTION
        WHEN insufficient_privilege OR duplicate_object THEN NULL;
    END;

    -- Detect unique constraint on (company_id, transaction_id, document_id)
    SELECT EXISTS (
        SELECT 1
        FROM pg_constraint c
        JOIN pg_class t ON t.oid = c.conrelid AND t.relname = 'reconciliations'
        JOIN pg_namespace n ON n.oid = t.relnamespace AND n.nspname = 'public'
        WHERE c.contype = 'u'
          AND (
              SELECT array_agg(attname::TEXT ORDER BY attnum)
              FROM pg_attribute
              WHERE attrelid = t.oid
                AND attnum = ANY (c.conkey)
          ) = ARRAY['company_id','transaction_id','document_id']::TEXT[]
    ) INTO v_has_unique;

    IF v_has_unique THEN 
        v_sql := $f$
WITH tx AS (
    SELECT
        t.transaction_id,
        t.company_id,
        t.currency,
        t.fiscal_year,
        t.quarter,
        t.month,
        NULLIF(regexp_replace(t.debited::TEXT, '[^\d\.-]', '', 'g'), '')::NUMERIC AS debited_num
    FROM public.transactions t
    WHERE NULLIF(regexp_replace(t.debited::TEXT, '[^\d\.-]', '', 'g'), '') IS NOT NULL
      AND COALESCE(t.reconciled, FALSE) = FALSE -- ignore reconciled transactions
)
INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, fiscal_year, quarter, month
)
SELECT
    $1::TEXT,
    tx.transaction_id::uuid,
    d.document_id::uuid,
    tx.company_id::uuid,
    tx.fiscal_year,
    tx.quarter,
    tx.month
FROM tx
JOIN public.documents_v2 d
  ON ROUND(d.total::NUMERIC, 2) = ROUND(tx.debited_num, 2)
 AND (d.currency IS NULL OR UPPER(d.currency) = UPPER(tx.currency))
 AND COALESCE(d.reconciled, FALSE) = FALSE -- ignore reconciled documents
WHERE tx.company_id::uuid = d.company_id::uuid
ON CONFLICT (company_id, transaction_id, document_id) DO NOTHING
$f$;

    ELSE 
        v_sql := $f$
WITH tx AS (
    SELECT
        t.transaction_id,
        t.company_id,
        t.currency,
        t.fiscal_year,
        t.quarter,
        t.month,
        NULLIF(regexp_replace(t.debited::TEXT, '[^\d\.-]', '', 'g'), '')::NUMERIC AS debited_num
    FROM public.transactions t
    WHERE NULLIF(regexp_replace(t.debited::TEXT, '[^\d\.-]', '', 'g'), '') IS NOT NULL
      AND COALESCE(t.reconciled, FALSE) = FALSE -- ignore reconciled transactions
),
src AS (
    SELECT
        tx.transaction_id,
        d.document_id,
        tx.company_id,
        tx.fiscal_year,
        tx.quarter,
        tx.month
    FROM tx
    JOIN public.documents_v2 d
      ON ROUND(d.total::NUMERIC, 2) = ROUND(tx.debited_num, 2)
     AND (d.currency IS NULL OR UPPER(d.currency) = UPPER(tx.currency))
     AND COALESCE(d.reconciled, FALSE) = FALSE -- ignore reconciled documents
    WHERE tx.company_id::uuid = d.company_id::uuid
)
INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, fiscal_year, quarter, month
)
SELECT
    $1::TEXT,
    s.transaction_id::uuid,
    s.document_id::uuid,
    s.company_id::uuid,
    s.fiscal_year,
    s.quarter,
    s.month
FROM src s
WHERE NOT EXISTS (
    SELECT 1
    FROM public.reconciliations r
    WHERE r.company_id     = s.company_id::uuid
      AND r.transaction_id = s.transaction_id::uuid
      AND r.document_id    = s.document_id::uuid
)
$f$;
    END IF;

    -- Execute the INSERT and stamp processing_time on the newly inserted rows.
    -- We avoid assuming a reconciliation_id column exists by joining back on the unique key.
    BEGIN
        EXECUTE
            'WITH ins AS (' || v_sql || ' RETURNING company_id, transaction_id, document_id ), ' ||
            'upd AS ( ' ||
            '  UPDATE public.reconciliations r ' ||
            '     SET processing_time = $2 ' ||
            '    FROM ins i ' ||
            '   WHERE r.company_id = i.company_id ' ||
            '     AND r.transaction_id = i.transaction_id ' ||
            '     AND r.document_id = i.document_id ' ||
            '  RETURNING 1 ' ||
            ') ' ||
            'SELECT count(*) FROM ins'
        USING p_athena_view, v_ms
        INTO v_rows;
    EXCEPTION
        WHEN OTHERS THEN 
            v_status := 'error';
            v_msg := SQLERRM;
    END;

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

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

alter function athena_reconcile_v5(unknown) owner to postgres;

grant execute on function athena_reconcile_v5(unknown) to anon;

grant execute on function athena_reconcile_v5(unknown) to authenticated;

grant execute on function athena_reconcile_v5(unknown) to service_role;

create or replace function stage_reconciliations_v2() returns jsonb
    language plpgsql
as
$$
DECLARE 
  v_rows    integer := 0;
  v_status  text    := 'ok';
  v_msg     text    := 'success';
  v_start   timestamptz := clock_timestamp();
  v_end     timestamptz;
  v_ms      bigint;
  v_sql     text;
  v_run_id  uuid;  -- logging run id
BEGIN
  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => 'stage_reconciliations_v2',
    p_company_id       => NULL,
    p_context          => jsonb_build_object('target_table','reconciliations_staging')
  );

  -- Build insert SQL (includes company_id)
  v_sql := $f$
    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
    )
  $f$;

  BEGIN
    EXECUTE v_sql;
    GET DIAGNOSTICS v_rows = ROW_COUNT;

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

  EXCEPTION WHEN OTHERS THEN
    v_status := 'error';
    v_msg := SQLERRM;

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

  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  => 'stage_reconciliations_v2',
    p_status            => (CASE WHEN v_status = 'ok' THEN 'ok' ELSE 'error' END)::text,
    p_message           => (CASE WHEN v_status = 'ok' THEN NULL ELSE v_msg END),
    p_processing_ms     => v_ms,
    p_context           => jsonb_build_object('reconciliations_staged', v_rows::int)
  );

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

alter function stage_reconciliations_v2() owner to postgres;

grant execute on function stage_reconciliations_v2() to anon;

grant execute on function stage_reconciliations_v2() to authenticated;

grant execute on function stage_reconciliations_v2() to service_role;

create or replace function create_notification(p_table_name text, p_action text, p_resource_id text, p_resource_ref text, p_company_id uuid, p_user_id text DEFAULT NULL::text) returns void
    language plpgsql
as
$$
declare
  notif_type record;
  user_pref record;
  global_enabled boolean;
begin
  -- 0. Check admin_config toggle
  select enable_notification_generation
  into global_enabled
  from admin_config
  limit 1;

  if not found or global_enabled = false then
    insert into notification_logs (status, action, resource_type, resource_ref, company_id, user_id, message)
    values ('ERROR', p_action, p_table_name, p_resource_ref, p_company_id, p_user_id, 'Notification generation disabled in admin_config');
    return;
  end if;

  -- 1. Find matching notification type
  select *
  into notif_type
  from notification_types
  where action = p_action
    and resource_type = p_table_name
    and enabled = true
  limit 1;

  if not found then
    insert into notification_logs (status, action, resource_type, resource_ref, company_id, user_id, message)
    values ('ERROR', p_action, p_table_name, p_resource_ref, p_company_id, p_user_id, 'Notification type not found or disabled');
    return;
  end if;

  -- 2. Loop through eligible users
  for user_pref in
    select *
    from notification_settings
    where company_id = p_company_id
      and enabled = true
  loop
    begin
      -- 3. Insert notification
  insert into notifications (
    id,
    user_id,
    company_id,
    title,
    message,
    type,
    action,
    target,
    system_message
  ) values (
    nextval(pg_get_serial_sequence('notifications','id')),  -- force safe sequence
    user_pref.user_id,
    p_company_id,
    notif_type.title,
    notif_type.description,
    notif_type.resource_type,
    p_action,
    p_resource_ref,
    true
  );


      -- 4. Log success
      insert into notification_logs (status, action, resource_type, resource_ref, company_id, user_id, notification_type_id, message)
      values ('SUCCESS', p_action, p_table_name, p_resource_ref, p_company_id, user_pref.user_id, notif_type.id, 'Notification created successfully');

    exception when others then
      -- 5. Log error if insert fails
      insert into notification_logs (status, action, resource_type, resource_ref, company_id, user_id, notification_type_id, message)
      values ('ERROR', p_action, p_table_name, p_resource_ref, p_company_id, user_pref.user_id, notif_type.id, SQLERRM);
    end;
  end loop;
end;
$$;

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

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

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

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

create or replace function generate_create_table(p_table_name text, p_schema_name text DEFAULT 'public'::text) returns text
    language plpgsql
as
$$
DECLARE
    v_sql         text;
    v_cols        text;
    v_constraints text;
BEGIN
    -- ensure table exists
    IF NOT EXISTS (
        SELECT 1
        FROM information_schema.tables
        WHERE table_schema = p_schema_name
          AND table_name   = p_table_name
    ) THEN
        RAISE EXCEPTION 'Table %.% does not exist', p_schema_name, p_table_name;
    END IF;

    -- Build column definitions
    SELECT string_agg(
               '    ' || quote_ident(c.column_name) || ' ' ||
               -- data type with length/precision when applicable
               (
                   CASE
                       WHEN c.data_type IN ('character varying','character','bit','bit varying')
                            AND c.character_maximum_length IS NOT NULL
                           THEN c.data_type || '(' || c.character_maximum_length || ')'
                       WHEN c.data_type IN ('numeric','decimal')
                            AND c.numeric_precision IS NOT NULL
                           THEN c.data_type || '(' || c.numeric_precision ||
                                COALESCE(',' || c.numeric_scale, '') || ')'
                       ELSE c.data_type
                   END
               ) ||
               -- identity handling
               CASE
                   WHEN c.is_identity = 'YES'
                       THEN ' GENERATED ' || c.identity_generation || ' AS IDENTITY'
                   ELSE ''
               END ||
               -- default value (skip for identity)
               CASE
                   WHEN c.column_default IS NOT NULL AND c.is_identity = 'NO'
                       THEN ' DEFAULT ' || c.column_default
                   ELSE ''
               END ||
               -- nullability
               CASE
                   WHEN c.is_nullable = 'NO'
                       THEN ' NOT NULL'
                   ELSE ''
               END
               , E',\n'
               ORDER BY c.ordinal_position
           )
    INTO v_cols
    FROM information_schema.columns c
    WHERE c.table_schema = p_schema_name
      AND c.table_name   = p_table_name;

    -- Build constraints (PRIMARY KEY, UNIQUE) without nested aggregates
    SELECT string_agg(
               '    CONSTRAINT ' || quote_ident(tc.constraint_name) || ' ' ||
               CASE tc.constraint_type
                   WHEN 'PRIMARY KEY' THEN 'PRIMARY KEY'
                   WHEN 'UNIQUE'      THEN 'UNIQUE'
               END || ' (' ||
               (
                   SELECT string_agg(quote_ident(k2.column_name), ', ' ORDER BY k2.ordinal_position)
                   FROM information_schema.key_column_usage k2
                   WHERE k2.constraint_name = tc.constraint_name
                     AND k2.table_schema    = tc.table_schema
                     AND k2.table_name      = tc.table_name
               )
               || ')'
               , E',\n'
               ORDER BY tc.constraint_name
           )
    INTO v_constraints
    FROM information_schema.table_constraints tc
    WHERE tc.table_schema   = p_schema_name
      AND tc.table_name     = p_table_name
      AND tc.constraint_type IN ('PRIMARY KEY','UNIQUE');

    -- Assemble full SQL
    v_sql := 'CREATE TABLE '
             || quote_ident(p_schema_name) || '.' || quote_ident(p_table_name) || E' (\n'
             || v_cols
             || COALESCE(E',\n' || v_constraints, '')
             || E'\n);\n';

    RETURN v_sql;
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'v_reconciled_totals'::text, p_date_window_days integer DEFAULT 7, p_amount_tolerance numeric DEFAULT 0.01, p_require_currency boolean DEFAULT false, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH weights AS (
    SELECT *
    FROM public.reconciliations_confidence_weights
    WHERE ($1 IS NULL OR weightset_id = $1)
    ORDER BY weightset_id
    LIMIT 1
),
/* --------------------- SOURCE NORMALIZATION --------------------- */
tx AS (
    SELECT
        t.transaction_id::uuid                 AS transaction_id,
        t.company_id::text                     AS company_id_txt,     -- TEXT in transactions
        t.completed_date::bigint               AS completed_date,     -- unix seconds
        UPPER(NULLIF(t.currency, ''))          AS currency_norm_tx,
        NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
        UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        t.merchant_id::text                    AS merchant_id_txt,    -- compare as TEXT
        COALESCE(t.reconciled, FALSE)          AS reconciled
    FROM public.transactions t
    WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
    SELECT
        d.document_id::uuid                    AS document_id,
        d.company_id::uuid                     AS company_id_uuid,    -- UUID in documents_v2
        /* Robust TEXT -> unix seconds conversion (handles '', 10/13-digit unix, ISO-ish, and DD/MM/YYYY) */
        CASE
            WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
            WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
            WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
            /* ISO-like: 2025-08-30[ T]12:34[:56][Z|+/-..] */
            WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
              THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
            /* DD/MM/YYYY (common in IE); safe only when day > 12 to avoid ambiguity */
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
                 AND split_part(btrim(d.document_date), '/', 1)::int > 12
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
            /* MM/DD/YYYY (fallback when month <= 12 and day <= 12) */
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
            ELSE NULL
        END::bigint                            AS document_unix,      -- unix seconds
        UPPER(NULLIF(d.currency, ''))          AS currency_norm,
        ROUND(d.total::numeric, 2)             AS total_num,
        UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        d.merchant_id::text                    AS merchant_id_txt,    -- compare as TEXT
        COALESCE(d.reconciled, FALSE)          AS reconciled
    FROM public.documents_v2 d
    WHERE COALESCE(d.reconciled, FALSE) = FALSE
),
/* --------------------- CANDIDATES (must match at least one field) --------------------- */
candidates AS (
    SELECT
        tx.transaction_id,
        docs.document_id,
        tx.company_id_txt,
        docs.company_id_uuid,
        tx.completed_date,
        docs.document_unix,
        tx.currency_norm_tx,
        docs.currency_norm,
        tx.debited_num,
        docs.total_num,
        tx.invoice_nr_norm AS tx_invoice,
        docs.invoice_nr_norm AS doc_invoice,
        tx.order_id_norm AS tx_order,
        docs.order_id_norm AS doc_order,
        tx.merchant_id_txt  AS tx_merchant,
        docs.merchant_id_txt AS doc_merchant
    FROM tx
    JOIN docs
      ON tx.company_id_txt = docs.company_id_uuid::text    -- TEXT = TEXT
     AND (
            ABS(tx.debited_num - docs.total_num) <= $4
         OR (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm)
         OR (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm)
         OR (tx.merchant_id_txt IS NOT NULL AND docs.merchant_id_txt IS NOT NULL AND tx.merchant_id_txt = docs.merchant_id_txt)
         OR (tx.completed_date IS NOT NULL AND docs.document_unix  IS NOT NULL AND ABS((docs.document_unix - tx.completed_date) / 86400) <= $3)
         OR (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
        )
     AND (
            ($5 = FALSE)  -- optional hard requirement for currency equality
         OR (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
        )
),
/* --------------------- FIELD MATCH FLAGS --------------------- */
scored AS (
    SELECT
        c.*,
        (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
        (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
        (c.doc_merchant IS NOT NULL AND c.tx_merchant IS NOT NULL AND c.doc_merchant = c.tx_merchant) AS merchant_match,
        (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ABS(c.debited_num - c.total_num) <= $4) AS amount_match,
        (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
        (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL AND ABS((c.document_unix - c.completed_date) / 86400) <= $3) AS date_match
    FROM candidates c
),
/* --------------------- WEIGHTED SCORING --------------------- */
weighted AS (
    SELECT
        s.transaction_id,
        s.document_id,
        s.company_id_txt,
        s.company_id_uuid,
        w.weightset_id,
        /* numerator: sum of weights for matched fields */
        (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
        (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
        (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
        (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
        (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
        (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END) AS numerator,
        /* denominator: only fields present on both sides count */
        (CASE WHEN s.document_unix IS NOT NULL AND s.completed_date IS NOT NULL THEN w.date           ELSE 0 END) +
        (CASE WHEN s.doc_invoice  IS NOT NULL AND s.tx_invoice  IS NOT NULL     THEN w.invoice_number ELSE 0 END) +
        (CASE WHEN s.doc_order    IS NOT NULL AND s.tx_order    IS NOT NULL     THEN w.order_number   ELSE 0 END) +
        (CASE WHEN s.doc_merchant IS NOT NULL AND s.tx_merchant IS NOT NULL     THEN w.merchant_id    ELSE 0 END) +
        (CASE WHEN s.debited_num  IS NOT NULL AND s.total_num   IS NOT NULL     THEN w.amount_total   ELSE 0 END) +
        (CASE WHEN s.currency_norm_tx IS NOT NULL AND s.currency_norm IS NOT NULL THEN w.currency     ELSE 0 END) AS denominator,
        /* tie-break helpers */
        ABS(COALESCE(s.debited_num,0) - COALESCE(s.total_num,0))                    AS amount_diff_abs,
        ABS(COALESCE((s.document_unix - s.completed_date) / 86400, 1000000))::int   AS date_diff_days
    FROM scored s
    CROSS JOIN weights w
),
conf AS (
    SELECT
        w.*,
        CASE WHEN w.denominator > 0
             THEN ROUND(100.0 * w.numerator / w.denominator, 2)
             ELSE 0 END AS confidence
    FROM weighted w
),
/* --------------------- PICK BEST DOC PER TX --------------------- */
best AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
        c.company_id_txt,
        c.company_id_uuid,
        c.transaction_id,
        c.document_id,
        c.weightset_id,
        c.confidence,
        c.amount_diff_abs,
        c.date_diff_days
    FROM conf c
    WHERE c.confidence >= $6
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             amount_diff_abs ASC,
             date_diff_days ASC,
             document_id
),
/* --------------------- INSERT (NO ON CONFLICT; dedupe via NOT EXISTS) --------------------- */
ins AS (
    INSERT INTO public.reconciliations (
        athena_view, transaction_id, document_id, company_id, confidence, weightset_id
    )
    SELECT
        $2::text,
        b.transaction_id::uuid,
        b.document_id::uuid,
        b.company_id_uuid,     -- insert as UUID from documents_v2
        b.confidence,
        b.weightset_id
    FROM best b
    WHERE NOT EXISTS (
        SELECT 1
        FROM public.reconciliations r
        WHERE r.company_id     = b.company_id_uuid
          AND r.transaction_id = b.transaction_id
          AND r.document_id    = b.document_id
    )
    RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    -- Execute with params in the same order as placeholders
    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3
            p_amount_tolerance, -- $4
            p_require_currency, -- $5
            p_min_confidence    -- $6
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

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

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v2(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'v_reconciled_totals'::text, p_date_window_days integer DEFAULT 7, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH weights AS (
    SELECT *,
           (date + invoice_number + order_number + merchant_id + amount_total + currency) AS total_weight
    FROM public.reconciliations_confidence_weights
    WHERE ($1 IS NULL OR weightset_id = $1)
    ORDER BY weightset_id
    LIMIT 1
),

/* --------- SOURCE NORMALIZATION --------- */
tx AS (
    SELECT
        t.transaction_id::uuid                 AS transaction_id,
        t.company_id::text                     AS company_id_txt,
        t.completed_date::bigint               AS completed_date,
        UPPER(NULLIF(t.currency, ''))          AS currency_norm_tx,
        NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
        UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        t.merchant_id::text                    AS merchant_id_txt,
        COALESCE(t.reconciled, FALSE)          AS reconciled
    FROM public.transactions t
    WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
    SELECT
        d.document_id::uuid                    AS document_id,
        d.company_id::uuid                     AS company_id_uuid,
        CASE
            WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
            WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
            WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
            WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
              THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
                 AND split_part(btrim(d.document_date), '/', 1)::int > 12
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
            ELSE NULL
        END::bigint                            AS document_unix,
        UPPER(NULLIF(d.currency, ''))          AS currency_norm,
        ROUND(d.total::numeric, 2)             AS total_num,
        UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        d.merchant_id::text                    AS merchant_id_txt,
        COALESCE(d.reconciled, FALSE)          AS reconciled
    FROM public.documents_v2 d
    WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* --------- CANDIDATES: company AND at least one field --------- */
candidates AS (
    SELECT
        tx.transaction_id,
        docs.document_id,
        tx.company_id_txt,
        docs.company_id_uuid,
        tx.completed_date,
        docs.document_unix,
        tx.currency_norm_tx,
        docs.currency_norm,
        tx.debited_num,
        docs.total_num,
        tx.invoice_nr_norm AS tx_invoice,
        docs.invoice_nr_norm AS doc_invoice,
        tx.order_id_norm AS tx_order,
        docs.order_id_norm AS doc_order,
        tx.merchant_id_txt  AS tx_merchant,
        docs.merchant_id_txt AS doc_merchant
    FROM tx
    JOIN docs
      ON tx.company_id_txt = docs.company_id_uuid::text
     AND (
            ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2)
         OR (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm)
         OR (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm)
         OR (tx.merchant_id_txt IS NOT NULL AND docs.merchant_id_txt IS NOT NULL AND tx.merchant_id_txt = docs.merchant_id_txt)
         OR (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
             AND ABS((docs.document_unix - tx.completed_date) / 86400) <= $3)
         OR (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
        )
),

/* --------- MATCH FLAGS --------- */
scored AS (
    SELECT
        c.*,
        (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
        (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
        (c.doc_merchant IS NOT NULL AND c.tx_merchant IS NOT NULL AND c.doc_merchant = c.tx_merchant) AS merchant_match,
        (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) AS amount_match,
        (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
        (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL AND ABS((c.document_unix - c.completed_date) / 86400) <= $3) AS date_match
    FROM candidates c
),

/* --------- CONFIDENCE (fixed denominator = total weight) --------- */
conf AS (
    SELECT
        s.transaction_id,
        s.document_id,
        s.company_id_txt,
        s.company_id_uuid,
        w.weightset_id,
        w.total_weight,
        w.currency AS currency_weight,
        /* numerator once, reuse for confidence and tie-break */
        (
          (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
          (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
          (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
          (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
          (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
          (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
        ) AS numerator,
        CASE WHEN w.total_weight > 0
             THEN ROUND(100.0 * (
                    (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
                    (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
                    (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
                    (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
                    (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
                    (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
                  ) / w.total_weight, 2)
             ELSE 0 END AS confidence,
        ABS(COALESCE((s.document_unix - s.completed_date) / 86400, 1000000))::int AS date_diff_days
    FROM scored s
    CROSS JOIN weights w
),

/* --------- PICK BEST DOC PER TX --------- */
best AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
        c.company_id_txt,
        c.company_id_uuid,
        c.transaction_id,
        c.document_id,
        c.weightset_id,
        c.confidence
    FROM conf c
    WHERE c.confidence >= $4
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             /* weak nudge when only currency is missing */
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
),

/* --------- INSERT (dedupe via NOT EXISTS) --------- */
ins AS (
    INSERT INTO public.reconciliations (
        athena_view, transaction_id, document_id, company_id, confidence, weightset_id
    )
    SELECT
        $2::text,
        b.transaction_id::uuid,
        b.document_id::uuid,
        b.company_id_uuid,
        b.confidence,
        b.weightset_id
    FROM best b
    WHERE NOT EXISTS (
        SELECT 1
        FROM public.reconciliations r
        WHERE r.company_id     = b.company_id_uuid
          AND r.transaction_id = b.transaction_id
          AND r.document_id    = b.document_id
    )
    RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3
            p_min_confidence    -- $4
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

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

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v3(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'v_reconciled_totals'::text, p_date_window_days integer DEFAULT 7, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH weights AS (
    SELECT *,
           (date + invoice_number + order_number + merchant_id + amount_total + currency) AS total_weight
    FROM public.reconciliations_confidence_weights
    WHERE ($1 IS NULL OR weightset_id = $1)
    ORDER BY weightset_id
    LIMIT 1
),

/* --------- SOURCE NORMALIZATION --------- */
tx AS (
    SELECT
        t.transaction_id::uuid                 AS transaction_id,
        t.company_id::text                     AS company_id_txt,
        t.completed_date::bigint               AS completed_date,
        UPPER(NULLIF(t.currency, ''))          AS currency_norm_tx,
        NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
        UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        t.merchant_id::text                    AS merchant_id_txt,
        COALESCE(t.reconciled, FALSE)          AS reconciled
    FROM public.transactions t
    WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
    SELECT
        d.document_id::uuid                    AS document_id,
        d.company_id::uuid                     AS company_id_uuid,
        CASE
            WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
            WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
            WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
            WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
              THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
                 AND split_part(btrim(d.document_date), '/', 1)::int > 12
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
            ELSE NULL
        END::bigint                            AS document_unix,
        UPPER(NULLIF(d.currency, ''))          AS currency_norm,
        ROUND(d.total::numeric, 2)             AS total_num,
        UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        d.merchant_id::text                    AS merchant_id_txt,
        COALESCE(d.reconciled, FALSE)          AS reconciled
    FROM public.documents_v2 d
    WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* --------- CANDIDATES: company AND at least one field --------- */
candidates AS (
    SELECT
        tx.transaction_id,
        docs.document_id,
        tx.company_id_txt,
        docs.company_id_uuid,
        tx.completed_date,
        docs.document_unix,
        tx.currency_norm_tx,
        docs.currency_norm,
        tx.debited_num,
        docs.total_num,
        tx.invoice_nr_norm AS tx_invoice,
        docs.invoice_nr_norm AS doc_invoice,
        tx.order_id_norm AS tx_order,
        docs.order_id_norm AS doc_order,
        tx.merchant_id_txt  AS tx_merchant,
        docs.merchant_id_txt AS doc_merchant
    FROM tx
    JOIN docs
      ON tx.company_id_txt = docs.company_id_uuid::text
     AND (
            ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2)
         OR (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm)
         OR (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm)
         OR (tx.merchant_id_txt IS NOT NULL AND docs.merchant_id_txt IS NOT NULL AND tx.merchant_id_txt = docs.merchant_id_txt)
         OR (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
             AND ABS((docs.document_unix - tx.completed_date) / 86400) <= $3)
         OR (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
        )
),

/* --------- MATCH FLAGS --------- */
scored AS (
    SELECT
        c.*,
        (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
        (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
        (c.doc_merchant IS NOT NULL AND c.tx_merchant IS NOT NULL AND c.doc_merchant = c.tx_merchant) AS merchant_match,
        (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) AS amount_match,
        (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
        (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL AND ABS((c.document_unix - c.completed_date) / 86400) <= $3) AS date_match
    FROM candidates c
),

/* --------- CONFIDENCE (fixed denominator = total weight) --------- */
conf AS (
    SELECT
        s.transaction_id,
        s.document_id,
        s.company_id_txt,
        s.company_id_uuid,
        w.weightset_id,
        w.total_weight,
        w.currency AS currency_weight,
        /* numerator once, reuse for confidence and tie-break */
        (
          (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
          (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
          (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
          (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
          (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
          (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
        ) AS numerator,
        CASE WHEN w.total_weight > 0
             THEN ROUND(100.0 * (
                    (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
                    (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
                    (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
                    (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
                    (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
                    (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
                  ) / w.total_weight, 2)
             ELSE 0 END AS confidence,
        ABS(COALESCE((s.document_unix - s.completed_date) / 86400, 1000000))::int AS date_diff_days
    FROM scored s
    CROSS JOIN weights w
),

/* --------- PICK BEST DOC PER TX --------- */
best AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
        c.company_id_txt,
        c.company_id_uuid,
        c.transaction_id,
        c.document_id,
        c.weightset_id,
        c.confidence
    FROM conf c
    WHERE c.confidence >= $4
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             /* weak nudge when only currency is missing */
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
),

/* --------- INSERT (dedupe via NOT EXISTS) --------- */
ins AS (
    INSERT INTO public.reconciliations (
        athena_view, transaction_id, document_id, company_id, confidence, weightset_id
    )
    SELECT
        $2::text,
        b.transaction_id::uuid,
        b.document_id::uuid,
        b.company_id_uuid,
        b.confidence,
        b.weightset_id
    FROM best b
    WHERE NOT EXISTS (
        SELECT 1
        FROM public.reconciliations r
        WHERE r.company_id     = b.company_id_uuid
          AND r.transaction_id = b.transaction_id
          AND r.document_id    = b.document_id
    )
    RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3
            p_min_confidence    -- $4
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

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

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v4(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v4'::text, p_date_window_days integer DEFAULT 7, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH weights AS (
    SELECT *,
           (date + invoice_number + order_number + merchant_id + amount_total + currency) AS total_weight
    FROM public.reconciliations_confidence_weights
    WHERE ($1 IS NULL OR weightset_id = $1)
    ORDER BY weightset_id
    LIMIT 1
),

/* --------- SOURCE NORMALIZATION --------- */
tx AS (
    SELECT
        t.transaction_id::uuid                 AS transaction_id,
        t.company_id::text                     AS company_id_txt,
        t.completed_date::bigint               AS completed_date,     -- unix seconds
        UPPER(NULLIF(t.currency, ''))          AS currency_norm_tx,
        NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
        UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        t.merchant_id::text                    AS merchant_id_txt,
        COALESCE(t.reconciled, FALSE)          AS reconciled
    FROM public.transactions t
    WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
    SELECT
        d.document_id::uuid                    AS document_id,
        d.company_id::uuid                     AS company_id_uuid,
        CASE
            WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
            WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
            WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
            WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
              THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
                 AND split_part(btrim(d.document_date), '/', 1)::int > 12
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
            ELSE NULL
        END::bigint                            AS document_unix,      -- unix seconds
        UPPER(NULLIF(d.currency, ''))          AS currency_norm,
        ROUND(d.total::numeric, 2)             AS total_num,
        UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        d.merchant_id::text                    AS merchant_id_txt,
        COALESCE(d.reconciled, FALSE)          AS reconciled
    FROM public.documents_v2 d
    WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* --------- CANDIDATES: company AND at least one field --------- */
candidates AS (
    SELECT
        tx.transaction_id,
        docs.document_id,
        tx.company_id_txt,
        docs.company_id_uuid,
        tx.completed_date,
        docs.document_unix,
        tx.currency_norm_tx,
        docs.currency_norm,
        tx.debited_num,
        docs.total_num,
        tx.invoice_nr_norm AS tx_invoice,
        docs.invoice_nr_norm AS doc_invoice,
        tx.order_id_norm AS tx_order,
        docs.order_id_norm AS doc_order,
        tx.merchant_id_txt  AS tx_merchant,
        docs.merchant_id_txt AS doc_merchant
    FROM tx
    JOIN docs
      ON tx.company_id_txt = docs.company_id_uuid::text
     AND (
            ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2)
         OR (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm)
         OR (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm)
         OR (tx.merchant_id_txt IS NOT NULL AND docs.merchant_id_txt IS NOT NULL AND tx.merchant_id_txt = docs.merchant_id_txt)
         OR (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
             AND FLOOR(docs.document_unix / 86400)::bigint = FLOOR(tx.completed_date / 86400)::bigint)  -- SAME DAY (UTC)
         OR (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
        )
),

/* --------- MATCH FLAGS --------- */
scored AS (
    SELECT
        c.*,
        (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
        (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
        (c.doc_merchant IS NOT NULL AND c.tx_merchant IS NOT NULL AND c.doc_merchant = c.tx_merchant) AS merchant_match,
        (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) AS amount_match,
        (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
        (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
         AND FLOOR(c.document_unix / 86400)::bigint = FLOOR(c.completed_date / 86400)::bigint) AS date_match  -- SAME DAY (UTC)
    FROM candidates c
),

/* --------- CONFIDENCE (fixed denominator = total weight) --------- */
conf AS (
    SELECT
        s.transaction_id,
        s.document_id,
        s.company_id_txt,
        s.company_id_uuid,
        w.weightset_id,
        w.total_weight,
        /* expose weights so we can store both weights and per-field scores */
        w.date           AS date_weight,
        w.invoice_number AS invoice_weight,
        w.order_number   AS order_weight,
        w.merchant_id    AS merchant_weight,
        w.amount_total   AS amount_weight,
        w.currency       AS currency_weight,

        /* flags */
        (s.date_match)     AS date_match,
        (s.invoice_match)  AS invoice_match,
        (s.order_match)    AS order_match,
        (s.merchant_match) AS merchant_match,
        (s.amount_match)   AS amount_match,
        (s.currency_match) AS currency_match,

        /* per-field scores (weight if matched else 0) */
        (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) AS date_score,
        (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) AS invoice_score,
        (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) AS order_score,
        (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) AS merchant_score,
        (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) AS amount_score,
        (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END) AS currency_score,

        /* numerator & confidence */
        (
          (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
          (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
          (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
          (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
          (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
          (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
        ) AS numerator,
        CASE WHEN w.total_weight > 0
             THEN ROUND(100.0 * (
                    (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
                    (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
                    (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
                    (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
                    (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
                    (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
                  ) / w.total_weight, 2)
             ELSE 0 END AS confidence,

        ABS(COALESCE((s.document_unix - s.completed_date) / 86400.0, 1000000))::int AS date_diff_days
    FROM scored s
    CROSS JOIN weights w
),

/* --------- PICK BEST DOC PER TX --------- */
best AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
        c.company_id_txt,
        c.company_id_uuid,
        c.transaction_id,
        c.document_id,
        c.weightset_id,
        c.confidence
    FROM conf c
    WHERE c.confidence >= $4
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
),

/* --------- INSERT reconciliations (dedupe via NOT EXISTS) --------- */
ins AS (
    INSERT INTO public.reconciliations (
        athena_view, transaction_id, document_id, company_id, confidence, weightset_id
    )
    SELECT
        $2::text,
        b.transaction_id::uuid,
        b.document_id::uuid,
        b.company_id_uuid,
        b.confidence,
        b.weightset_id
    FROM best b
    WHERE NOT EXISTS (
        SELECT 1
        FROM public.reconciliations r
        WHERE r.company_id     = b.company_id_uuid
          AND r.transaction_id = b.transaction_id
          AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
),

/* --------- INSERT per-field scores linked by reconciliation_id --------- */
ms AS (
    SELECT
        i.reconciliation_id,
        c.weightset_id,
        c.confidence,
        c.numerator,
        c.total_weight,
        /* flags */
        c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match,
        /* weights */
        c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight,
        /* scores */
        c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score
    FROM ins i
    JOIN conf c
      ON c.company_id_uuid = i.company_id
     AND c.transaction_id  = i.transaction_id
     AND c.document_id     = i.document_id
),
ins_ms AS (
    INSERT INTO public.reconciliations_match_scores (
        reconciliation_id,
        weightset_id,
        confidence,
        numerator,
        total_weight,
        date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
        date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
        date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
    )
    SELECT
        reconciliation_id,
        weightset_id,
        confidence,
        numerator,
        total_weight,
        date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
        date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
        date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
    FROM ms
    ON CONFLICT (reconciliation_id) DO UPDATE
      SET weightset_id   = EXCLUDED.weightset_id,
          confidence     = EXCLUDED.confidence,
          numerator      = EXCLUDED.numerator,
          total_weight   = EXCLUDED.total_weight,
          date_match     = EXCLUDED.date_match,
          invoice_match  = EXCLUDED.invoice_match,
          order_match    = EXCLUDED.order_match,
          merchant_match = EXCLUDED.merchant_match,
          amount_match   = EXCLUDED.amount_match,
          currency_match = EXCLUDED.currency_match,
          date_weight    = EXCLUDED.date_weight,
          invoice_weight = EXCLUDED.invoice_weight,
          order_weight   = EXCLUDED.order_weight,
          merchant_weight= EXCLUDED.merchant_weight,
          amount_weight  = EXCLUDED.amount_weight,
          currency_weight= EXCLUDED.currency_weight,
          date_score     = EXCLUDED.date_score,
          invoice_score  = EXCLUDED.invoice_score,
          order_score    = EXCLUDED.order_score,
          merchant_score = EXCLUDED.merchant_score,
          amount_score   = EXCLUDED.amount_score,
          currency_score = EXCLUDED.currency_score
    RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3 (ignored by same-day logic; kept for signature)
            p_min_confidence    -- $4
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

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

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v5(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v5'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH
fallback_w AS (
  SELECT
    date           AS fw_date,
    invoice_number AS fw_invoice,
    order_number   AS fw_order,
    merchant_id    AS fw_merchant,
    amount_total   AS fw_amount,
    currency       AS fw_currency
  FROM public.reconciliations_confidence_weights
  ORDER BY weightset_id
  LIMIT 1
),

reg_base_raw AS (
  SELECT weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active
),
reg_company_raw AS (
  SELECT company_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
),
reg_merchant_raw AS (
  SELECT company_id, merchant_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE active
),

/* FIXED: wrap fw.fw_* in MAX() so everything is aggregated */
reg_base AS (
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), MAX(fw.fw_date))     AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), MAX(fw.fw_invoice))  AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), MAX(fw.fw_order))    AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), MAX(fw.fw_merchant)) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), MAX(fw.fw_amount))   AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), MAX(fw.fw_currency)) AS base_currency
  FROM reg_base_raw b
  CROSS JOIN fallback_w fw
),
reg_company AS (
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency
  FROM reg_company_raw
  GROUP BY company_id
),
reg_merchant AS (
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency
  FROM reg_merchant_raw
  GROUP BY company_id, merchant_id
),

/* -------- source normalization -------- */
tx AS (
  SELECT
    t.transaction_id::uuid AS transaction_id,
    t.company_id::text     AS company_id_txt,
    t.completed_date::bigint AS completed_date,
    UPPER(NULLIF(t.currency, '')) AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
    t.merchant_id::text AS merchant_id_txt
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
  SELECT
    d.document_id::uuid AS document_id,
    d.company_id::uuid  AS company_id_uuid,
    CASE
      WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
      WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
      WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
      WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
        THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
      WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
           AND split_part(btrim(d.document_date), '/', 1)::int > 12
        THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
      WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
        THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
      ELSE NULL
    END::bigint AS document_unix,
    UPPER(NULLIF(d.currency, '')) AS currency_norm,
    ROUND(d.total::numeric, 2)    AS total_num,
    UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
    d.merchant_id::text AS merchant_id_txt
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* -------- candidates: company must match + at least one field match -------- */
candidates AS (
  SELECT
    tx.transaction_id, docs.document_id,
    tx.company_id_txt, docs.company_id_uuid,
    tx.completed_date, docs.document_unix,
    tx.currency_norm_tx, docs.currency_norm,
    tx.debited_num, docs.total_num,
    tx.invoice_nr_norm AS tx_invoice,  docs.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm  AS tx_order,     docs.order_id_norm   AS doc_order,
    tx.merchant_id_txt AS tx_merchant, docs.merchant_id_txt AS doc_merchant
  FROM tx
  JOIN docs
    ON tx.company_id_txt = docs.company_id_uuid::text
   AND (
        ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2) OR
        (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm) OR
        (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm) OR
        (tx.merchant_id_txt IS NOT NULL AND docs.merchant_id_txt IS NOT NULL AND tx.merchant_id_txt = docs.merchant_id_txt) OR
        (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
           AND ABS( (docs.document_unix - tx.completed_date) / 86400.0 ) <= $3) OR
        (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
      )
),

/* -------- resolve weights per candidate: merchant → company → base -------- */
resolved_weights AS (
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant, c.tx_merchant) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight
  FROM candidates c
  CROSS JOIN reg_base rb
  LEFT JOIN reg_company rc
    ON rc.company_id = c.company_id_uuid
  LEFT JOIN reg_merchant rm
    ON rm.company_id = c.company_id_uuid
   AND rm.merchant_id = COALESCE(c.doc_merchant, c.tx_merchant)
),

/* -------- flags -------- */
scored AS (
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
    (c.doc_merchant IS NOT NULL AND c.tx_merchant IS NOT NULL AND c.doc_merchant = c.tx_merchant) AS merchant_match,
    (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND ABS( (c.document_unix - c.completed_date) / 86400.0 ) <= $3) AS date_match
  FROM candidates c
),

/* -------- compute numerator / denominator / confidence -------- */
conf AS (
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight) AS total_weight,

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

    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,

    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END)
    ) AS numerator,

    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight), 2)
         ELSE 0 END AS confidence,

    ABS( (s.document_unix - s.completed_date) / 86400.0 ) AS date_diff_days
  FROM scored s
  JOIN resolved_weights rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id
),

/* -------- pick best doc per tx -------- */
best AS (
  SELECT DISTINCT ON (company_id_txt, transaction_id)
    c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
    c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days
  FROM conf c
  WHERE c.confidence >= $4
  ORDER BY company_id_txt, transaction_id,
           confidence DESC,
           (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
           date_diff_days ASC,
           document_id
),

/* -------- insert reconciliations -------- */
ins AS (
  INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, confidence, weightset_id
  )
  SELECT
    $2::text,
    b.transaction_id, b.document_id, b.company_id_uuid,
    b.confidence, $1::uuid
  FROM best b
  WHERE NOT EXISTS (
    SELECT 1 FROM public.reconciliations r
    WHERE r.company_id = b.company_id_uuid
      AND r.transaction_id = b.transaction_id
      AND r.document_id = b.document_id
  )
  RETURNING reconciliation_id, company_id, transaction_id, document_id
),

/* -------- write per-field scores -------- */
ms AS (
  SELECT
    i.reconciliation_id,
    $1::uuid AS weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score
  FROM ins i
  JOIN conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id
),
ins_ms AS (
  INSERT INTO public.reconciliations_match_scores (
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
  )
  SELECT
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
  FROM ms
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score
  RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3
            p_min_confidence    -- $4
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

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

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function reconciliations_weight_registry_upsert(p_weight_key text, p_weight_value numeric, p_company_id uuid DEFAULT NULL::uuid, p_merchant_id text DEFAULT NULL::text, p_active boolean DEFAULT true) returns reconciliations_weight_registry
    language plpgsql
as
$$
DECLARE
  v_row public.reconciliations_weight_registry;
  v_merchant text;
BEGIN
  v_merchant := NULLIF(btrim(p_merchant_id), '');

  IF p_weight_key NOT IN (
       'date','invoice_number','order_number','merchant_id','amount_total','currency','customer_name'
     ) THEN
    RAISE EXCEPTION
      'weight_key must be one of (date, invoice_number, order_number, merchant_id, amount_total, currency, customer_name). Got: %',
      p_weight_key USING ERRCODE = '22023';
  END IF;

  IF p_weight_value IS NULL OR p_weight_value < 0 THEN
    RAISE EXCEPTION 'weight_value must be non-negative. Got: %', p_weight_value USING ERRCODE = '22003';
  END IF;

  INSERT INTO public.reconciliations_weight_registry (
      company_id, merchant_id, weight_key, weight_value, active
  )
  VALUES (p_company_id, v_merchant, p_weight_key, p_weight_value, p_active)
  ON CONFLICT (company_id, merchant_id, weight_key)
  DO UPDATE SET weight_value = EXCLUDED.weight_value, active = EXCLUDED.active
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v6(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v6'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH
fallback_w AS (
  SELECT
    date           AS fw_date,
    invoice_number AS fw_invoice,
    order_number   AS fw_order,
    merchant_id    AS fw_merchant,
    amount_total   AS fw_amount,
    currency       AS fw_currency
  FROM public.reconciliations_confidence_weights
  ORDER BY weightset_id
  LIMIT 1
),

reg_base_raw AS (
  SELECT weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active
),
reg_company_raw AS (
  SELECT company_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
),
reg_merchant_raw AS (
  SELECT company_id, merchant_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE active
),

reg_base AS (
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), MAX(fw.fw_date))     AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), MAX(fw.fw_invoice))  AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), MAX(fw.fw_order))    AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), MAX(fw.fw_merchant)) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), MAX(fw.fw_amount))   AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), MAX(fw.fw_currency)) AS base_currency
  FROM reg_base_raw b
  CROSS JOIN fallback_w fw
),
reg_company AS (
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency
  FROM reg_company_raw
  GROUP BY company_id
),
reg_merchant AS (
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency
  FROM reg_merchant_raw
  GROUP BY company_id, merchant_id
),

/* ---------- normalization (uses transactions.name_beneficiary) ---------- */
tx AS (
  SELECT
    t.transaction_id::uuid   AS transaction_id,
    t.company_id::text       AS company_id_txt,
    t.completed_date::bigint AS completed_date,
    UPPER(NULLIF(t.currency, '')) AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,

    t.merchant_id::text AS tx_merchant_raw,

    /* beneficiary name normalized for fuzzy merchant match */
    UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), ''))                        AS benef_upper,
    regexp_replace(
      UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), '')),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    )                                                                                                         AS benef_core,
    REPLACE(regexp_replace(
      btrim(regexp_replace(
        UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), '')),
        '\s+', ' ', 'g'
      )),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    ), ' ', '')                                                                                               AS benef_core_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
  SELECT
    d.document_id::uuid AS document_id,
    d.company_id::uuid  AS company_id_uuid,

    CASE
      WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
      WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
      WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
      WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
        THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
      WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
           AND split_part(btrim(d.document_date), '/', 1)::int > 12
        THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
      WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
        THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
      ELSE NULL
    END::bigint AS document_unix,

    UPPER(NULLIF(d.currency, '')) AS currency_norm,
    ROUND(d.total::numeric, 2)    AS total_num,
    UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,

    /* use merchant_name (not merchant_id) for merchant matching */
    d.merchant_name::text AS doc_merchant_raw,

    UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), ''))                        AS merch_upper,
    regexp_replace(
      UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), '')),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    )                                                                                                      AS merch_core,
    REPLACE(regexp_replace(
      btrim(regexp_replace(
        UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), '')),
        '\s+', ' ', 'g'
      )),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    ), ' ', '')                                                                                            AS merch_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* candidates: company must match + any one field matches */
candidates AS (
  SELECT
    tx.transaction_id, docs.document_id,
    tx.company_id_txt, docs.company_id_uuid,
    tx.completed_date, docs.document_unix,
    tx.currency_norm_tx, docs.currency_norm,
    tx.debited_num, docs.total_num,
    tx.invoice_nr_norm AS tx_invoice,  docs.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm  AS tx_order,     docs.order_id_norm   AS doc_order,

    tx.tx_merchant_raw,
    docs.doc_merchant_raw,

    tx.benef_core,  tx.benef_core_ns,
    docs.merch_core, docs.merch_core_ns,

    tx.fiscal_year, tx.quarter, tx.month_text
  FROM tx
  JOIN docs
    ON tx.company_id_txt = docs.company_id_uuid::text
   AND (
        ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2) OR
        (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm) OR
        (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm) OR

        /* fuzzy merchant: Discord Inc ↔ DISCORD, contains either way */
        (
          docs.merch_core_ns IS NOT NULL AND (
               (tx.benef_core_ns IS NOT NULL AND (tx.benef_core_ns = docs.merch_core_ns
                                               OR POSITION(docs.merch_core_ns IN tx.benef_core_ns) > 0
                                               OR POSITION(tx.benef_core_ns  IN docs.merch_core_ns) > 0))
            OR (tx.tx_merchant_raw IS NOT NULL AND
                POSITION(docs.merch_core IN UPPER(regexp_replace(tx.tx_merchant_raw, '[^A-Z0-9 ]','', 'g'))) > 0)
          )
        ) OR

        (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
           AND ABS( (docs.document_unix - tx.completed_date) / 86400.0 ) <= $3) OR
        (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
      )
),

/* resolve weights for this scope */
resolved_weights AS (
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight
  FROM candidates c
  CROSS JOIN reg_base rb
  LEFT JOIN reg_company rc
    ON rc.company_id = c.company_id_uuid
  LEFT JOIN reg_merchant rm
    ON rm.company_id = c.company_id_uuid
   AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw)
),

/* flags (reuse fuzzy merchant logic for the boolean) */
scored AS (
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,

    (
      c.merch_core_ns IS NOT NULL AND (
           (c.benef_core_ns IS NOT NULL AND (c.benef_core_ns = c.merch_core_ns
                                          OR POSITION(c.merch_core_ns IN c.benef_core_ns) > 0
                                          OR POSITION(c.benef_core_ns  IN c.merch_core_ns) > 0))
        OR (c.tx_merchant_raw IS NOT NULL AND
            POSITION(c.merch_core IN UPPER(regexp_replace(c.tx_merchant_raw, '[^A-Z0-9 ]','', 'g'))) > 0)
      )
    ) AS merchant_match,

    (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm)     AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND ABS( (c.document_unix - c.completed_date) / 86400.0 ) <= $3)                                          AS date_match
  FROM candidates c
),

/* compute confidence */
conf AS (
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight) AS total_weight,

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

    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,

    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END)
    ) AS numerator,

    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight), 2)
         ELSE 0 END AS confidence,

    ABS( (s.document_unix - s.completed_date) / 86400.0 ) AS date_diff_days
  FROM scored s
  JOIN resolved_weights rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id
),

/* pick best doc per tx */
best AS (
  SELECT DISTINCT ON (company_id_txt, transaction_id)
    c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
    c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
    c.fiscal_year, c.quarter, c.month_text
  FROM conf c
  WHERE c.confidence >= $4
  ORDER BY company_id_txt, transaction_id,
           confidence DESC,
           (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
           date_diff_days ASC,
           document_id
),

/* insert reconciliations (+ fiscal fields) */
ins AS (
  INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
    fiscal_year, quarter, month
  )
  SELECT
    $2::text,
    b.transaction_id, b.document_id, b.company_id_uuid,
    b.confidence, $1::uuid,
    b.fiscal_year, b.quarter, b.month_text
  FROM best b
  WHERE NOT EXISTS (
    SELECT 1 FROM public.reconciliations r
    WHERE r.company_id = b.company_id_uuid
      AND r.transaction_id = b.transaction_id
      AND r.document_id = b.document_id
  )
  RETURNING reconciliation_id, company_id, transaction_id, document_id
),

/* write per-field breakdown */
ms AS (
  SELECT
    i.reconciliation_id,
    $1::uuid AS weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score
  FROM ins i
  JOIN conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id
),
ins_ms AS (
  INSERT INTO public.reconciliations_match_scores (
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
  )
  SELECT
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
  FROM ms
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score
  RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3
            p_min_confidence    -- $4
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

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

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v7(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v7'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH
fallback_w AS (
  SELECT
    date           AS fw_date,
    invoice_number AS fw_invoice,
    order_number   AS fw_order,
    merchant_id    AS fw_merchant,
    amount_total   AS fw_amount,
    currency       AS fw_currency
  FROM public.reconciliations_confidence_weights
  ORDER BY weightset_id
  LIMIT 1
),

reg_base_raw AS (
  SELECT weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active
),
reg_company_raw AS (
  SELECT company_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
),
reg_merchant_raw AS (
  SELECT company_id, merchant_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE active
),

/* base/company/merchant weights, incl. customer_name (default 0 if absent) */
reg_base AS (
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), MAX(fw.fw_date))     AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), MAX(fw.fw_invoice))  AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), MAX(fw.fw_order))    AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), MAX(fw.fw_merchant)) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), MAX(fw.fw_amount))   AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), MAX(fw.fw_currency)) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0)                   AS base_customer
  FROM reg_base_raw b
  CROSS JOIN fallback_w fw
),
reg_company AS (
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM reg_company_raw
  GROUP BY company_id
),
reg_merchant AS (
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM reg_merchant_raw
  GROUP BY company_id, merchant_id
),

/* ---------- normalization ---------- */
tx AS (
  SELECT
    t.transaction_id::uuid   AS transaction_id,
    t.company_id::text       AS company_id_txt,
    t.completed_date::bigint AS completed_date,
    UPPER(NULLIF(t.currency, '')) AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,

    t.merchant_id::text AS tx_merchant_raw,

    /* beneficiary for merchant fuzzy (kept) */
    UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), ''))                        AS benef_upper,
    regexp_replace(
      UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), '')),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    )                                                                                                         AS benef_core,
    REPLACE(regexp_replace(
      btrim(regexp_replace(
        UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), '')),
        '\s+', ' ', 'g'
      )),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    ), ' ', '')                                                                                               AS benef_core_ns,

    /* NEW: consumer name for customer_name signal */
    UPPER(NULLIF(regexp_replace(t.name_consumer::text, '[^A-Z0-9 ]', '', 'g'), ''))                           AS consumer_upper,
    REPLACE(
      btrim(regexp_replace(UPPER(NULLIF(regexp_replace(t.name_consumer::text, '[^A-Z0-9 ]', '', 'g'), '')), '\s+', ' ', 'g')),
      ' ', ''
    )                                                                                                         AS consumer_core_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
  SELECT
    d.document_id::uuid AS document_id,
    d.company_id::uuid  AS company_id_uuid,

    /* Robust date → unix seconds:
       1) use document_date_unix if present
       2) support 13/10 digit epochs
       3) support ISO YYYY-MM-DD...
       4) support DD/MM/YYYY and MM/DD/YYYY
       5) support YYYY-Mon-DD and YYYY-Month-DD (e.g. 2025-Aug-31, 2025-August-31)
    */
    COALESCE(
      NULLIF(d.document_date_unix, 0)::bigint,
      CASE
        WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
        WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
        WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
        WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
          THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
        WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
             AND split_part(btrim(d.document_date), '/', 1)::int > 12
          THEN EXTRACT(EPOCH FROM (to_timestamp(btrim(d.document_date), 'DD/MM/YYYY') AT TIME ZONE 'UTC'))
        WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
          THEN EXTRACT(EPOCH FROM (to_timestamp(btrim(d.document_date), 'MM/DD/YYYY') AT TIME ZONE 'UTC'))
        /* NEW: YYYY-Mon-DD, e.g. 2025-Aug-31 */
        WHEN btrim(d.document_date) ~ '^\d{4}-[A-Za-z]{3}-\d{1,2}$'
          THEN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(d.document_date), '-', ' '), 'YYYY Mon DD') AT TIME ZONE 'UTC'))
        /* NEW: YYYY-Month-DD, e.g. 2025-August-31 */
        WHEN btrim(d.document_date) ~ '^\d{4}-[A-Za-z]+-\d{1,2}$'
          THEN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(d.document_date), '-', ' '), 'YYYY Month DD') AT TIME ZONE 'UTC'))
        ELSE NULL
      END
    )::bigint AS document_unix,

    UPPER(NULLIF(d.currency, '')) AS currency_norm,
    ROUND(d.total::numeric, 2)    AS total_num,
    UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,

    /* merchant_name normalized (for merchant signal) */
    d.merchant_name::text AS doc_merchant_raw,
    UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), ''))                        AS merch_upper,
    regexp_replace(
      UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), '')),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    )                                                                                                      AS merch_core,
    REPLACE(regexp_replace(
      btrim(regexp_replace(
        UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), '')),
        '\s+', ' ', 'g'
      )),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    ), ' ', '')                                                                                            AS merch_core_ns,

    /* customer_info normalized to plain text for containment search */
    UPPER(NULLIF(regexp_replace(d.customer_info::text, '[^A-Z0-9 ]', '', 'g'), ''))                        AS custinfo_upper,
    REPLACE(
      btrim(regexp_replace(UPPER(NULLIF(regexp_replace(d.customer_info::text, '[^A-Z0-9 ]', '', 'g'), '')), '\s+', ' ', 'g')),
      ' ', ''
    )                                                                                                      AS custinfo_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* candidates: company must match + any one field matches */
candidates AS (
  SELECT
    tx.transaction_id, docs.document_id,
    tx.company_id_txt, docs.company_id_uuid,
    tx.completed_date, docs.document_unix,
    tx.currency_norm_tx, docs.currency_norm,
    tx.debited_num, docs.total_num,
    tx.invoice_nr_norm AS tx_invoice,  docs.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm  AS tx_order,     docs.order_id_norm   AS doc_order,

    tx.tx_merchant_raw,
    docs.doc_merchant_raw,
    tx.benef_core,  tx.benef_core_ns,
    docs.merch_core, docs.merch_core_ns,

    /* consumer/customer normalized fields */
    tx.consumer_core_ns,
    docs.custinfo_core_ns,

    tx.fiscal_year, tx.quarter, tx.month_text
  FROM tx
  JOIN docs
    ON tx.company_id_txt = docs.company_id_uuid::text
   AND (
        ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2) OR
        (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm) OR
        (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm) OR

        /* fuzzy merchant: contains either way */
        (
          docs.merch_core_ns IS NOT NULL AND (
               (tx.benef_core_ns IS NOT NULL AND (tx.benef_core_ns = docs.merch_core_ns
                                               OR POSITION(docs.merch_core_ns IN tx.benef_core_ns) > 0
                                               OR POSITION(tx.benef_core_ns  IN docs.merch_core_ns) > 0))
            OR (tx.tx_merchant_raw IS NOT NULL AND
                POSITION(docs.merch_core IN UPPER(regexp_replace(tx.tx_merchant_raw, '[^A-Z0-9 ]','', 'g'))) > 0)
          )
        ) OR

        /* customer_name signal — consumer within customer_info (::text normalized) */
        (
          tx.consumer_core_ns IS NOT NULL AND docs.custinfo_core_ns IS NOT NULL AND
          ( tx.consumer_core_ns = docs.custinfo_core_ns
            OR POSITION(tx.consumer_core_ns IN docs.custinfo_core_ns) > 0
            OR POSITION(docs.custinfo_core_ns IN tx.consumer_core_ns) > 0 )
        ) OR

        (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
           AND ABS( (docs.document_unix - tx.completed_date) / 86400.0 ) <= $3) OR
        (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
      )
),

/* resolve weights for this scope (incl. customer_name) */
resolved_weights AS (
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM candidates c
  CROSS JOIN reg_base rb
  LEFT JOIN reg_company rc
    ON rc.company_id = c.company_id_uuid
  LEFT JOIN reg_merchant rm
    ON rm.company_id = c.company_id_uuid
   AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw)
),

/* flags (add customer_name) */
scored AS (
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,

    (
      c.merch_core_ns IS NOT NULL AND (
           (c.benef_core_ns IS NOT NULL AND (c.benef_core_ns = c.merch_core_ns
                                          OR POSITION(c.merch_core_ns IN c.benef_core_ns) > 0
                                          OR POSITION(c.benef_core_ns  IN c.merch_core_ns) > 0))
        OR (c.tx_merchant_raw IS NOT NULL AND
            POSITION(c.merch_core IN UPPER(regexp_replace(c.tx_merchant_raw, '[^A-Z0-9 ]','', 'g'))) > 0)
      )
    ) AS merchant_match,

    /* customer_name boolean */
    (
      c.consumer_core_ns IS NOT NULL AND c.custinfo_core_ns IS NOT NULL AND
      ( c.consumer_core_ns = c.custinfo_core_ns
        OR POSITION(c.consumer_core_ns IN c.custinfo_core_ns) > 0
        OR POSITION(c.custinfo_core_ns IN c.consumer_core_ns) > 0 )
    ) AS customer_match,

    (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm)     AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND ABS( (c.document_unix - c.completed_date) / 86400.0 ) <= $3)                                          AS date_match
  FROM candidates c
),

/* compute confidence (include customer_name) */
conf AS (
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,

    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,

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

    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,

    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,

    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,

    ABS( (s.document_unix - s.completed_date) / 86400.0 ) AS date_diff_days
  FROM scored s
  JOIN resolved_weights rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id
),

/* pick best doc per tx */
best AS (
  SELECT DISTINCT ON (company_id_txt, transaction_id)
    c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
    c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
    c.fiscal_year, c.quarter, c.month_text
  FROM conf c
  WHERE c.confidence >= $4
  ORDER BY company_id_txt, transaction_id,
           confidence DESC,
           (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
           date_diff_days ASC,
           document_id
),

/* insert reconciliations (+ fiscal fields) */
ins AS (
  INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
    fiscal_year, quarter, month
  )
  SELECT
    $2::text,
    b.transaction_id, b.document_id, b.company_id_uuid,
    b.confidence, $1::uuid,
    b.fiscal_year, b.quarter, b.month_text
  FROM best b
  WHERE NOT EXISTS (
    SELECT 1 FROM public.reconciliations r
    WHERE r.company_id = b.company_id_uuid
      AND r.transaction_id = b.transaction_id
      AND r.document_id = b.document_id
  )
  RETURNING reconciliation_id, company_id, transaction_id, document_id
),

/* write per-field breakdown (adds customer_* columns) */
ms AS (
  SELECT
    i.reconciliation_id,
    $1::uuid AS weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,

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

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

    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id
),
ins_ms AS (
  INSERT INTO public.reconciliations_match_scores (
    reconciliation_id,
    weightset_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
    reconciliation_id,
    weightset_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
  FROM ms
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score
  RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,
            p_athena_view,
            p_date_window_days,
            p_min_confidence
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

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

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function norm_core_ns(p text) returns text
    immutable
    strict
    parallel safe
    language sql
as
$$
  SELECT REPLACE(
           regexp_replace(
             btrim(
               regexp_replace(
                 regexp_replace(upper(p), '[^A-Z0-9 ]', '', 'g'),
                 '\s+', ' ', 'g'
               )
             ),
             '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M',
             '',
             'g'
           ),
           ' ',
           ''
         )
$$;

alter function norm_core_ns(unknown) owner to postgres;

grant execute on function norm_core_ns(unknown) to anon;

grant execute on function norm_core_ns(unknown) to authenticated;

grant execute on function norm_core_ns(unknown) to service_role;

create or replace function norm_code(p text) returns text
    immutable
    strict
    parallel safe
    language sql
as
$$
  SELECT NULLIF(regexp_replace(upper(p), '[^A-Z0-9]', '', 'g'), '')
$$;

alter function norm_code(unknown) owner to postgres;

grant execute on function norm_code(unknown) to anon;

grant execute on function norm_code(unknown) to authenticated;

grant execute on function norm_code(unknown) to service_role;

create or replace function norm_text_ns(p text) returns text
    immutable
    strict
    parallel safe
    language sql
as
$$
  SELECT REPLACE(
           btrim(
             regexp_replace(
               regexp_replace(upper(p), '[^A-Z0-9 ]', '', 'g'),
               '\s+', ' ', 'g'
             )
           ),
           ' ',
           ''
         )
$$;

alter function norm_text_ns(unknown) owner to postgres;

grant execute on function norm_text_ns(unknown) to anon;

grant execute on function norm_text_ns(unknown) to authenticated;

grant execute on function norm_text_ns(unknown) to service_role;

create or replace function parse_doc_date(p_date text, p_unix bigint) returns bigint
    stable
    language plpgsql
as
$$
BEGIN
  IF p_unix IS NOT NULL AND p_unix <> 0 THEN RETURN p_unix; END IF;
  IF p_date IS NULL OR btrim(p_date) = '' THEN RETURN NULL; END IF;

  IF btrim(p_date) ~ '^\d{13}$' THEN
    RETURN (btrim(p_date))::bigint / 1000;
  ELSIF btrim(p_date) ~ '^\d{10}$' THEN
    RETURN (btrim(p_date))::bigint;
  ELSIF btrim(p_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$' THEN
    RETURN EXTRACT(EPOCH FROM (btrim(p_date)::timestamptz))::bigint;
  ELSIF btrim(p_date) ~ '^\d{4}-[A-Za-z]{3}-\d{1,2}$' THEN
    RETURN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(p_date), '-', ' '), 'YYYY Mon DD') AT TIME ZONE 'UTC'))::bigint;
  ELSIF btrim(p_date) ~ '^\d{4}-[A-Za-z]+-\d{1,2}$' THEN
    RETURN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(p_date), '-', ' '), 'YYYY Month DD') AT TIME ZONE 'UTC'))::bigint;
  ELSIF btrim(p_date) ~ '^\d{1,2}-[A-Za-z]{3}-\d{4}$' THEN
    RETURN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(p_date), '-', ' '), 'DD Mon YYYY') AT TIME ZONE 'UTC'))::bigint;
  ELSIF btrim(p_date) ~ '^\d{1,2}-[A-Za-z]+-\d{4}$' THEN
    RETURN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(p_date), '-', ' '), 'DD Month YYYY') AT TIME ZONE 'UTC'))::bigint;
  ELSIF btrim(p_date) ~ '^\d{2}/\d{2}/\d{4}$' THEN
    IF split_part(btrim(p_date), '/', 1)::int > 12 THEN
      RETURN EXTRACT(EPOCH FROM (to_timestamp(btrim(p_date), 'DD/MM/YYYY') AT TIME ZONE 'UTC'))::bigint;
    ELSE
      RETURN EXTRACT(EPOCH FROM (to_timestamp(btrim(p_date), 'MM/DD/YYYY') AT TIME ZONE 'UTC'))::bigint;
    END IF;
  END IF;

  RETURN NULL;
END;
$$;

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

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

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

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

create or replace function f_import_customers_from_yuki(p_company_id uuid, p_payload jsonb)
    returns TABLE(domain text, customer_id uuid, action text)
    security definer
    SET search_path = public
    language plpgsql
as
$$
declare
  r jsonb;
  v_domain text;
  v_id uuid;
  v_created_at timestamp;
begin
  if not exists (select 1 from public.companies c where c.company_id = p_company_id) then
    raise exception 'Company % not found in public.companies', p_company_id;
  end if;

  if p_payload ? 'data' is false then
    raise exception 'Payload must contain a "data" array';
  end if;

  for r in select jsonb_array_elements(p_payload->'data') loop
    v_domain := nullif(r->>'Domain','');
    if v_domain is null then continue; end if;

    begin
      v_id := nullif(r->>'ID','')::uuid;
    exception when invalid_text_representation then
      v_id := null;
    end;

    if r ? 'Created' and r->>'Created' not in ('', 'null') then
      v_created_at := to_date(r->>'Created','DD-MM-YYYY')::timestamp;
    else
      v_created_at := null;
    end if;

    return query
    insert into public.customers as c
      ( customer_id, company_id, name, domain, created_at )
    values
      ( v_id, p_company_id, v_domain, v_domain, v_created_at )
    on conflict on constraint customers_pkey1  -- <<<<<< avoids "customer_id" ambiguity
    do update
      set company_id = excluded.company_id,
          name       = excluded.name,
          domain     = excluded.domain,
          created_at = coalesce(excluded.created_at, c.created_at)
    returning
      c.domain,
      c.customer_id,                                   -- explicit table column
      case when c.xmax = 0 then 'insert' else 'update' end as action;
  end loop;
end
$$;

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

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

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

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

create or replace function athena_reconcile_general_v8(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v8'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  text;
    v_rows integer := 0;
    v_status text := 'ok';
    v_msg text := 'success';
    v_start timestamptz := clock_timestamp();
    v_end   timestamptz;
    v_ms    bigint;
BEGIN
v_sql := $f$
WITH
-- 1) Registry layers (base → company → merchant), registry-only
reg_base_raw AS (
  SELECT weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active
),
reg_company_raw AS (
  SELECT company_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
),
reg_merchant_raw AS (
  SELECT company_id, merchant_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE active
),
reg_base AS (
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM reg_base_raw
),
reg_company AS (
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM reg_company_raw
  GROUP BY company_id
),
reg_merchant AS (
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM reg_merchant_raw
  GROUP BY company_id, merchant_id
),

-- 2) Normalize sources (helper fns)
tx AS (
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,
    public.norm_core_ns(t.name_beneficiary) AS benef_core_ns,

    public.norm_text_ns(t.name_consumer)    AS consumer_core_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text                    AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND ($4 IS NULL OR t.company_id::uuid = $4)
),
docs AS (
  SELECT
    d.document_id::uuid             AS document_id,
    d.company_id::uuid              AS company_id_uuid,

    -- CAST numeric → bigint for parse_doc_date(p_date text, p_unix bigint)
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,

    UPPER(NULLIF(d.currency, ''))   AS currency_norm,
    ROUND(d.total::numeric, 2)      AS total_num,

    public.norm_code(d.invoice_number) AS invoice_nr_norm,
    public.norm_code(d.order_id)       AS order_id_norm,

    d.merchant_name::text              AS doc_merchant_raw,
    public.norm_core_ns(d.merchant_name)     AS merch_core_ns,

    public.norm_text_ns(d.customer_info::text) AS custinfo_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND ($4 IS NULL OR d.company_id = $4)
),

-- 3) Candidates: same company AND at least one signal
candidates AS (
  SELECT
    tx.transaction_id, docs.document_id,
    tx.company_id_txt, docs.company_id_uuid,
    tx.completed_date, docs.document_unix,
    tx.currency_norm_tx, docs.currency_norm,
    tx.debited_num, docs.total_num,
    tx.invoice_nr_norm AS tx_invoice,  docs.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,    docs.order_id_norm   AS doc_order,

    tx.tx_merchant_raw,  docs.doc_merchant_raw,
    tx.benef_core_ns,    docs.merch_core_ns,

    tx.consumer_core_ns, docs.custinfo_core_ns,

    tx.fiscal_year, tx.quarter, tx.month_text
  FROM tx
  JOIN docs
    ON tx.company_id_txt = docs.company_id_uuid::text
   AND (
        ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2) OR
        (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm) OR
        (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm) OR

        -- fuzzy merchant contains either way
        (
          docs.merch_core_ns IS NOT NULL AND tx.benef_core_ns IS NOT NULL AND
          ( tx.benef_core_ns = docs.merch_core_ns
            OR POSITION(docs.merch_core_ns IN tx.benef_core_ns) > 0
            OR POSITION(tx.benef_core_ns  IN docs.merch_core_ns) > 0 )
        ) OR

        -- customer name signal: consumer within customer_info text
        (
          tx.consumer_core_ns IS NOT NULL AND docs.custinfo_core_ns IS NOT NULL AND
          ( tx.consumer_core_ns = docs.custinfo_core_ns
            OR POSITION(tx.consumer_core_ns IN docs.custinfo_core_ns) > 0
            OR POSITION(docs.custinfo_core_ns IN tx.consumer_core_ns) > 0 )
        ) OR

        (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
           AND ABS( (docs.document_unix - tx.completed_date) / 86400.0 ) <= $2) OR
        (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
      )
),

-- 4) Resolve weights (merchant → company → base)
resolved_weights AS (
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM candidates c
  CROSS JOIN reg_base rb
  LEFT JOIN reg_company rc
    ON rc.company_id = c.company_id_uuid
  LEFT JOIN reg_merchant rm
    ON rm.company_id = c.company_id_uuid
   AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw)
),

-- 5) Flags
scored AS (
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,

    (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
      ( c.benef_core_ns = c.merch_core_ns
        OR POSITION(c.merch_core_ns IN c.benef_core_ns) > 0
        OR POSITION(c.benef_core_ns  IN c.merch_core_ns) > 0 )) AS merchant_match,

    (c.consumer_core_ns IS NOT NULL AND c.custinfo_core_ns IS NOT NULL AND
      ( c.consumer_core_ns = c.custinfo_core_ns
        OR POSITION(c.consumer_core_ns IN c.custinfo_core_ns) > 0
        OR POSITION(c.custinfo_core_ns IN c.consumer_core_ns) > 0 )) AS customer_match,

    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND ABS( (c.document_unix - c.completed_date) / 86400.0 ) <= $2) AS date_match
  FROM candidates c
),

-- 6) Confidence
conf AS (
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,

    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,

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

    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,

    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,

    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,

    ABS( (s.document_unix - s.completed_date) / 86400.0 ) AS date_diff_days
  FROM scored s
  JOIN resolved_weights rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id
),

-- 7) Pick best doc per tx
best AS (
  SELECT DISTINCT ON (company_id_txt, transaction_id)
    c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
    c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
    c.fiscal_year, c.quarter, c.month_text
  FROM conf c
  WHERE c.confidence >= $3
  ORDER BY company_id_txt, transaction_id,
           confidence DESC,
           (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
           date_diff_days ASC,
           document_id
),

-- 8) Insert reconciliations (weightset_id now NULL)
ins AS (
  INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
    fiscal_year, quarter, month
  )
  SELECT
    $1::text,
    b.transaction_id, b.document_id, b.company_id_uuid,
    b.confidence, NULL::uuid,
    b.fiscal_year, b.quarter, b.month_text
  FROM best b
  WHERE NOT EXISTS (
    SELECT 1 FROM public.reconciliations r
    WHERE r.company_id = b.company_id_uuid
      AND r.transaction_id = b.transaction_id
      AND r.document_id = b.document_id
  )
  RETURNING reconciliation_id, company_id, transaction_id, document_id
),

-- 9) Insert breakdown
ms AS (
  SELECT
    i.reconciliation_id,
    NULL::uuid AS weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,

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

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

    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id
),
ins_ms AS (
  INSERT INTO public.reconciliations_match_scores (
    reconciliation_id,
    weightset_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
    reconciliation_id,
    weightset_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
  FROM ms
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score
  RETURNING 1
)
SELECT count(*) FROM ins
$f$;

BEGIN
  EXECUTE v_sql
  USING
    p_athena_view,       -- $1
    p_date_window_days,  -- $2
    p_min_confidence,    -- $3
    p_company_id         -- $4
  INTO v_rows;
EXCEPTION WHEN OTHERS THEN
  v_status := 'error';
  v_msg := SQLERRM;
END;

v_end := clock_timestamp();
v_ms := floor(extract(epoch from (v_end - v_start)) * 1000)::bigint;

RETURN jsonb_build_object(
  'reconciliations_processed', v_rows,
  'status', v_status,
  'message', v_msg,
  'processing_time', v_ms
);
END;
$$;

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

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

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

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

create or replace function f_import_yuki_domains(p_payload jsonb)
    returns TABLE(domain text, yuki_domain_id uuid, action text)
    security definer
    SET search_path = public
    language plpgsql
as
$$
declare
  r jsonb;
  v_domain text;
begin
  if p_payload ? 'data' is false then
    raise exception 'Payload must contain a "data" array';
  end if;

  for r in select jsonb_array_elements(p_payload->'data') loop
    v_domain := r->>'Domain';
    if coalesce(v_domain,'') = '' then continue; end if;

    return query
    insert into public.yuki_domains as yd
      ( yuki_domain_id
      , domain
      , status_id
      , created
      , status
      , companies
      , workflow
      , questions
      , bobyparent
      , bundle
      , trial
      , signed_gdpr_document
      , allow_self_accounting
      , users
      , archived_on
      , inactive_on
      , is_overdue
      , portal
      )
    values
      (
        nullif(r->>'ID','')::uuid,
        v_domain,
        nullif(r->>'StatusId','')::int,
        case when r ? 'Created' and r->>'Created' not in ('', 'null') then to_date(r->>'Created','DD-MM-YYYY') end,
        nullif(r->>'Status',''),
        nullif(r->>'Companies','')::int,
        nullif(r->>'Workflow','')::int,
        nullif(r->>'Questions','')::int,
        coalesce(nullif(r->>'BOByParent','')::int, 0),
        nullif(r->>'Bundle',''),
        case
          when (r ? 'Trial') is false or r->>'Trial' = '' then null
          when lower(r->>'Trial') in ('true','t','yes','y','1') then true
          when lower(r->>'Trial') in ('false','f','no','n','0') then false
          else (nullif(r->>'Trial','')::int <> 0)
        end,
        (r->>'SignedGDPRDocument')::boolean,
        nullif(r->>'AllowSelfAccounting','')::int,
        nullif(r->>'Users','')::int,
        case when r ? 'ArchivedOn' and r->>'ArchivedOn' not in ('', 'null') then to_timestamp(r->>'ArchivedOn','DD-MM-YYYY')::timestamptz end,
        case when r ? 'InactiveOn' and r->>'InactiveOn' not in ('', 'null') then to_date(r->>'InactiveOn','DD-MM-YYYY') end,
        coalesce(
          nullif(r->>'IsOverdue','')::int,
          case when (r->>'IsOverdue')::boolean is true then 1
               when (r->>'IsOverdue')::boolean is false then 0 end
        ),
        nullif(r->>'Portal','')
      )
    on conflict (domain) do update
      set yuki_domain_id          = coalesce(excluded.yuki_domain_id, yd.yuki_domain_id),
          status_id               = excluded.status_id,
          created                 = excluded.created,
          status                  = excluded.status,
          companies               = excluded.companies,
          workflow                = excluded.workflow,
          questions               = excluded.questions,
          bobyparent              = excluded.bobyparent,
          bundle                  = excluded.bundle,
          trial                   = excluded.trial,
          signed_gdpr_document    = excluded.signed_gdpr_document,
          allow_self_accounting   = excluded.allow_self_accounting,
          users                   = excluded.users,
          archived_on             = excluded.archived_on,
          inactive_on             = excluded.inactive_on,
          is_overdue              = excluded.is_overdue,
          portal                  = excluded.portal
    returning
      yd.domain,
      yd.yuki_domain_id,
      case when yd.xmax = 0 then 'insert' else 'update' end;
  end loop;
end
$$;

alter function f_import_yuki_domains(unknown) owner to postgres;

grant execute on function f_import_yuki_domains(unknown) to anon;

grant execute on function f_import_yuki_domains(unknown) to authenticated;

grant execute on function f_import_yuki_domains(unknown) to service_role;

create or replace function athena_reconcile_general_v9_fast(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v9_fast'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';

  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  ---------------------------------------------------------------------------
  -- 0) REGISTRY SNAPSHOTS (small tables, cheap)
  ---------------------------------------------------------------------------
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    GET DIAGNOSTICS _count = ROW_COUNT;
    t1 := clock_timestamp();
    RAISE NOTICE 'registry snapshotted (% ms)', EXTRACT(MILLISECOND FROM t1 - t0);
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  ---------------------------------------------------------------------------
  -- 1) NORMALIZE SOURCES INTO TEMP TABLES + LOCAL INDEXES
  ---------------------------------------------------------------------------
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,
    public.norm_core_ns(t.name_beneficiary) AS benef_core_ns,
    public.norm_text_ns(t.name_consumer)    AS consumer_core_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text                    AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  -- numeric indexes: debited_num frequently used
  CREATE INDEX ON _tx (debited_num);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                          AS document_id,
    d.company_id::uuid                           AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,

    UPPER(NULLIF(d.currency, ''))                AS currency_norm,
    ROUND(d.total::numeric, 2)                   AS total_num,

    public.norm_code(d.invoice_number)           AS invoice_nr_norm,
    public.norm_code(d.order_id)                 AS order_id_norm,

    d.merchant_name::text                        AS doc_merchant_raw,
    public.norm_core_ns(d.merchant_name)         AS merch_core_ns,

    public.norm_text_ns(d.customer_info::text)   AS custinfo_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);

  IF p_debug THEN
    t2 := clock_timestamp();
    RAISE NOTICE '_tx/_docs built + indexed (% ms)', EXTRACT(MILLISECOND FROM t2 - t1);
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  ---------------------------------------------------------------------------
  -- 2) CANDIDATES via UNION ALL branches (no giant OR) + DISTINCT
  ---------------------------------------------------------------------------
  -- Amount match
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  -- Invoice code match
  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  -- Order code match
  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  -- Merchant fuzzy
  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  -- Customer name signal
  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND d.custinfo_core_ns IS NOT NULL
   AND (
        tx.consumer_core_ns = d.custinfo_core_ns
     OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
     OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0
   );

  -- Date window
  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  -- Currency equality
  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  -- Union all branches, dedupe once
  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id)
    *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL
    SELECT * FROM _cand_invoice
    UNION ALL
    SELECT * FROM _cand_order
    UNION ALL
    SELECT * FROM _cand_merchant
    UNION ALL
    SELECT * FROM _cand_customer
    UNION ALL
    SELECT * FROM _cand_date
    UNION ALL
    SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    RAISE NOTICE 'candidates built: % rows (% ms)', _count, EXTRACT(MILLISECOND FROM t1 - t2);
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  ---------------------------------------------------------------------------
  -- 3) RESOLVE WEIGHTS (merchant → company → base)
  ---------------------------------------------------------------------------
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  IF p_debug THEN
    t2 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _rw;
    RAISE NOTICE 'resolved_weights: % rows (% ms)', _count, EXTRACT(MILLISECOND FROM t2 - t1);
    j_timings := j_timings
      || jsonb_build_object('resolved_rows', _count)
      || jsonb_build_object('resolved_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  ---------------------------------------------------------------------------
  -- 4) FLAGS + CONFIDENCE (on temp tables)
  ---------------------------------------------------------------------------
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
    (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
      ( c.benef_core_ns = c.merch_core_ns
        OR position(c.merch_core_ns in c.benef_core_ns) > 0
        OR position(c.benef_core_ns  in c.merch_core_ns) > 0 )) AS merchant_match,
    (c.consumer_core_ns IS NOT NULL AND c.custinfo_core_ns IS NOT NULL AND
      ( c.consumer_core_ns = c.custinfo_core_ns
        OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
        OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 )) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,

    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,

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

    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,

    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,

    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,

    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _conf;
    RAISE NOTICE 'flags+conf: % rows (% ms)', _count, EXTRACT(MILLISECOND FROM t1 - t2);
    j_timings := j_timings
      || jsonb_build_object('conf_rows', _count)
      || jsonb_build_object('conf_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  ---------------------------------------------------------------------------
  -- 5) BEST per tx + INSERTS
  ---------------------------------------------------------------------------
  CREATE TEMP TABLE _best AS
  SELECT DISTINCT ON (company_id_txt, transaction_id)
    c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
    c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
    c.fiscal_year, c.quarter, c.month_text
  FROM _conf c
  WHERE c.confidence >= p_min_confidence
  ORDER BY company_id_txt, transaction_id,
           confidence DESC,
           (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
           date_diff_days ASC,
           document_id;

  IF p_debug THEN
    t2 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _best;
    RAISE NOTICE 'best: % rows (% ms)', _count, EXTRACT(MILLISECOND FROM t2 - t1);
    j_timings := j_timings
      || jsonb_build_object('best_rows', _count)
      || jsonb_build_object('best_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  -- Insert reconciliations
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, NULL::uuid,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliation_id,
    weightset_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
    i.reconciliation_id,
    NULL::uuid AS weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  IF p_debug THEN
    t1 := clock_timestamp();
    RAISE NOTICE 'inserted/updated ms rows: % (% ms)', v_rows, EXTRACT(MILLISECOND FROM t1 - t2);
    j_timings := j_timings
      || jsonb_build_object('inserted_rows', v_rows)
      || jsonb_build_object('insert_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  ---------------------------------------------------------------------------
  -- Done
  ---------------------------------------------------------------------------
  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v10(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v10'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight registry snapshots */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
    RAISE NOTICE 'registry: % ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint;
  END IF;

  /* 1) Normalize sources into temp tables + indexes */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    public.norm_core_ns(t.name_beneficiary) AS benef_core_ns,
    public.norm_text_ns(t.name_consumer)    AS consumer_core_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text                    AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    public.norm_core_ns(d.merchant_name)       AS merch_core_ns,
    public.norm_text_ns(d.customer_info::text) AS custinfo_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
    RAISE NOTICE 'normalize+index: % ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint;
  END IF;

  /* 2) Candidates via targeted branches (UNION ALL) */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND d.custinfo_core_ns IS NOT NULL
   AND (
        tx.consumer_core_ns = d.custinfo_core_ns
     OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
     OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
    RAISE NOTICE 'candidates: % rows, % ms', _count, ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint;
  END IF;

  /* 3) Resolve weights (merchant → company → base) */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  IF p_debug THEN
    t2 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _rw;
    j_timings := j_timings
      || jsonb_build_object('resolved_rows', _count)
      || jsonb_build_object('resolved_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
    RAISE NOTICE 'resolved: % rows, % ms', _count, ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint;
  END IF;

  /* 4) Flags + confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
    (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
      ( c.benef_core_ns = c.merch_core_ns
        OR position(c.merch_core_ns in c.benef_core_ns) > 0
        OR position(c.benef_core_ns  in c.merch_core_ns) > 0 )) AS merchant_match,
    (c.consumer_core_ns IS NOT NULL AND c.custinfo_core_ns IS NOT NULL AND
      ( c.consumer_core_ns = c.custinfo_core_ns
        OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
        OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 )) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _conf;
    j_timings := j_timings
      || jsonb_build_object('conf_rows', _count)
      || jsonb_build_object('conf_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
    RAISE NOTICE 'conf: % rows, % ms', _count, ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint;
  END IF;

  /* 5) Best per tx + inserts */
  CREATE TEMP TABLE _best AS
  SELECT DISTINCT ON (company_id_txt, transaction_id)
    c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
    c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
    c.fiscal_year, c.quarter, c.month_text
  FROM _conf c
  WHERE c.confidence >= p_min_confidence
  ORDER BY company_id_txt, transaction_id,
           confidence DESC,
           (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
           date_diff_days ASC,
           document_id;

  IF p_debug THEN
    t2 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _best;
    j_timings := j_timings
      || jsonb_build_object('best_rows', _count)
      || jsonb_build_object('best_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
    RAISE NOTICE 'best: % rows, % ms', _count, ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint;
  END IF;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, NULL::uuid,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliation_id,
    weightset_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
    i.reconciliation_id,
    NULL::uuid AS weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('inserted_rows', v_rows)
      || jsonb_build_object('insert_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
    RAISE NOTICE 'insert: % rows, % ms', v_rows, ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint;
  END IF;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function f_suggest_customer_domain(p_name text, p_company_id uuid DEFAULT NULL::uuid) returns text
    stable
    language sql
as
$$
with raw_name as (
  select coalesce(nullif(p_name, ''), 'customer') as n
),
base_slug as (
  select lower(
           regexp_replace(                       -- trim leading/trailing underscores
             regexp_replace(                     -- replace non-alnum with underscores
               unaccent(n),
               '[^a-zA-Z0-9]+', '_', 'g'
             ),
             '^_+|_+$', '', 'g'
           )
         ) as base
  from raw_name
),
norm as (
  select case when base = '' then 'customer' else base end as base from base_slug
),
candidates as (
  -- Try the base slug, then add suffixes _2, _3, ...
  select base as candidate, 0 as n from norm
  union all
  select base || '_' || gs.n::text as candidate, gs.n
  from norm, generate_series(2, 9999) as gs(n)
),
taken as (
  select lower(c.domain) as dkey
  from public.customers c
  where c.domain is not null
    and (p_company_id is null or c.company_id = p_company_id)
),
first_free as (
  select c.candidate
  from candidates c
  left join taken t on t.dkey = c.candidate
  where t.dkey is null
  order by c.n asc
  limit 1
)
select candidate from first_free;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v11(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v11'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weights snapshot (registry only) */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  /* 1) Normalize sources → temp tables + indexes */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    COALESCE(t.completed_date, t.paid_at::bigint, t.started_date::bigint) AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,
    public.norm_core_ns(t.name_beneficiary)                AS benef_core_ns,
    public.norm_core_ns(t.account_holder_name_counterparty) AS counterparty_core_ns,

    public.norm_code(t.description)            AS desc_code,
    public.norm_code(t.remittance_information) AS remit_code,
    public.norm_code(t.processed_description)  AS pdesc_code,

    public.norm_iban(t.iban_counterparty)      AS tx_iban_cp,
    public.norm_iban(t.iban_beneficiary)       AS tx_iban_ben,
    public.norm_code(t.bic_counterparty)       AS tx_bic_cp,
    public.norm_code(t.bic)                    AS tx_bic,

    public.norm_text_ns(t.name_consumer)       AS consumer_core_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text                               AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (counterparty_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_cp);
  CREATE INDEX ON _tx (tx_iban_ben);
  CREATE INDEX ON _tx (tx_bic_cp);
  CREATE INDEX ON _tx (tx_bic);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    public.norm_code(d.receipt_number)         AS receipt_nr_norm,

    d.merchant_name::text                      AS doc_merchant_raw,
    public.norm_core_ns(d.merchant_name)       AS merch_core_ns,

    public.norm_iban((d.merchant_info::jsonb ->> 'iban')) AS doc_iban,
    public.norm_code((d.merchant_info::jsonb ->> 'bic'))  AS doc_bic,

    (
      COALESCE( (d.totals_details::jsonb ->> 'total_paid')::numeric, d.total_paid::numeric, 0 )
      >=
      COALESCE( (d.totals_details::jsonb ->> 'total')::numeric, d.total::numeric, 0 )
    )
    OR
    COALESCE(d.amount_due::numeric, (d.totals_details::jsonb ->> 'amount_due')::numeric) = 0
    AS doc_paid,

    public.norm_text_ns(d.customer_info::text) AS custinfo_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (receipt_nr_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (doc_iban);
  CREATE INDEX ON _docs (doc_bic);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 2) Candidates via UNION branches */
  -- Amount
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm, d.total_num,
    d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order, d.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  -- Invoice exact
  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm, d.total_num,
    d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order, d.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL AND d.invoice_nr_norm = tx.invoice_nr_norm;

  -- Invoice via tx free-text tokens
  CREATE TEMP TABLE _cand_invoice_ref AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm, d.total_num,
    d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order, d.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.invoice_nr_norm IS NOT NULL
   AND (tx.desc_code  = d.invoice_nr_norm
     OR tx.remit_code = d.invoice_nr_norm
     OR tx.pdesc_code = d.invoice_nr_norm);

  -- Order exact (or receipt)
  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm, d.total_num,
    d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order, d.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND (
        (tx.order_id_norm IS NOT NULL AND d.order_id_norm   = tx.order_id_norm) OR
        (tx.order_id_norm IS NOT NULL AND d.receipt_nr_norm = tx.order_id_norm)
       );

  -- Merchant fuzzy (name)
  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm, d.total_num,
    d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order, d.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL AND d.merch_core_ns IS NOT NULL
   AND (tx.benef_core_ns = d.merch_core_ns
        OR position(d.merch_core_ns in tx.benef_core_ns) > 0
        OR position(tx.benef_core_ns in d.merch_core_ns) > 0);

  -- Bank identity (IBAN/BIC equality)
  CREATE TEMP TABLE _cand_bank AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm, d.total_num,
    d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order, d.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND (
        (tx.tx_iban_cp IS NOT NULL AND d.doc_iban IS NOT NULL AND tx.tx_iban_cp = d.doc_iban) OR
        (tx.tx_iban_ben IS NOT NULL AND d.doc_iban IS NOT NULL AND tx.tx_iban_ben = d.doc_iban) OR
        (tx.tx_bic_cp  IS NOT NULL AND d.doc_bic  IS NOT NULL AND tx.tx_bic_cp  = d.doc_bic) OR
        (tx.tx_bic     IS NOT NULL AND d.doc_bic  IS NOT NULL AND tx.tx_bic     = d.doc_bic)
       );

  -- Customer name
  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm, d.total_num,
    d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order, d.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL AND d.custinfo_core_ns IS NOT NULL
   AND (tx.consumer_core_ns = d.custinfo_core_ns
        OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
        OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0);

  -- Date window
  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm, d.total_num,
    d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order, d.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  -- Currency equality
  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm, d.total_num,
    d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order, d.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  -- Union + dedupe
  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_invoice_ref
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_bank
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 3) Resolve weights (merchant → company → base) */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  IF p_debug THEN
    t2 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _rw;
    j_timings := j_timings
      || jsonb_build_object('resolved_rows', _count)
      || jsonb_build_object('resolved_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 4) Flags + confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,

    -- invoice (exact or via ref tokens)
    (c.doc_invoice IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm)
    OR (c.doc_invoice IS NOT NULL AND (c.desc_code = c.doc_invoice OR c.remit_code = c.doc_invoice OR c.pdesc_code = c.doc_invoice))
    AS invoice_match,

    -- order/receipt
    (c.doc_order IS NOT NULL AND c.order_id_norm IS NOT NULL AND c.doc_order = c.order_id_norm)
    OR (c.doc_receipt IS NOT NULL AND c.order_id_norm IS NOT NULL AND c.doc_receipt = c.order_id_norm)
    AS order_match,

    -- merchant: fuzzy OR bank identity OR counterparty-name fuzzy
    (
      (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
        ( c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns  in c.merch_core_ns) > 0 ))
      OR
      (c.counterparty_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
        ( position(c.merch_core_ns in c.counterparty_core_ns) > 0
          OR position(c.counterparty_core_ns in c.merch_core_ns) > 0 ))
      OR
      (c.doc_iban IS NOT NULL AND (c.tx_iban_cp = c.doc_iban OR c.tx_iban_ben = c.doc_iban))
      OR
      (c.doc_bic  IS NOT NULL AND (c.tx_bic_cp  = c.doc_bic  OR c.tx_bic      = c.doc_bic))
    ) AS merchant_match,

    -- customer name
    (c.consumer_core_ns IS NOT NULL AND c.custinfo_core_ns IS NOT NULL AND
      ( c.consumer_core_ns = c.custinfo_core_ns
        OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
        OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 )) AS customer_match,

    -- amount/currency/date
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days,
    s.doc_paid
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _conf;
    j_timings := j_timings
      || jsonb_build_object('conf_rows', _count)
      || jsonb_build_object('conf_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 5) Best per transaction (paid tiebreaker) + inserts */
  CREATE TEMP TABLE _best AS
  SELECT DISTINCT ON (company_id_txt, transaction_id)
    c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
    c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
    c.fiscal_year, c.quarter, c.month_text, c.doc_paid
  FROM _conf c
  WHERE c.confidence >= p_min_confidence
  ORDER BY company_id_txt, transaction_id,
           confidence DESC,
           (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
           date_diff_days ASC,
           doc_paid DESC,
           document_id;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, NULL::uuid,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliation_id,
    weightset_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
    i.reconciliation_id,
    NULL::uuid AS weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v12(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v12'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight registry snapshots */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  /* 1) Normalize sources into temp tables + indexes */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,

    -- FIX: merchant/beneficiary fallback
    public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ) AS benef_core_ns,

    public.norm_text_ns(t.name_consumer) AS consumer_core_ns,

    -- IBAN/BIC (normalized for matching and candidate generation)
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text                    AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,

    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,

    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,

    d.merchant_name::text                      AS doc_merchant_raw,
    public.norm_core_ns(d.merchant_name)       AS merch_core_ns,

    public.norm_text_ns(d.customer_info::text) AS custinfo_core_ns,

    -- IBAN/BIC searchable text from merchant_info + customer_info
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 2) Candidates via targeted branches (UNION ALL) */
  -- amount
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  -- invoice code
  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  -- order code
  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  -- merchant fuzzy
  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  -- customer name
  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND d.custinfo_core_ns IS NOT NULL
   AND (
        tx.consumer_core_ns = d.custinfo_core_ns
     OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
     OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0
   );

  -- NEW: IBAN
  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND length(tx.tx_iban_ns) >= 15
     AND position(tx.tx_iban_ns in d.info_ns) > 0;

  -- NEW: BIC
  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND position(tx.tx_bic_ns in d.info_ns) > 0;

  -- date window
  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  -- currency equality
  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban     -- NEW
    UNION ALL SELECT * FROM _cand_bic      -- NEW
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 3) Resolve weights (merchant → company → base) */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  /* 4) Flags + confidence (IBAN/BIC contribute to merchant_match) */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,

    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,

    (
      -- merchant fuzzy name
      (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
        ( c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns  in c.merch_core_ns) > 0 ))
      -- OR: IBAN/BIC confirms merchant relation
      OR (c.tx_iban_ns IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_iban_ns in c.info_ns) > 0)
      OR (c.tx_bic_ns  IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_bic_ns  in c.info_ns) > 0)
    ) AS merchant_match,

    (c.consumer_core_ns IS NOT NULL AND c.custinfo_core_ns IS NOT NULL AND
      ( c.consumer_core_ns = c.custinfo_core_ns
        OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
        OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 )) AS customer_match,

    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,

    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,

    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,

    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,

    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  /* 5) Best per tx + inserts */
  CREATE TEMP TABLE _best AS
  SELECT DISTINCT ON (company_id_txt, transaction_id)
    c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
    c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
    c.fiscal_year, c.quarter, c.month_text
  FROM _conf c
  WHERE c.confidence >= p_min_confidence
  ORDER BY company_id_txt, transaction_id,
           confidence DESC,
           (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
           date_diff_days ASC,
           document_id;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, NULL::uuid,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliation_id,
    weightset_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
    i.reconciliation_id,
    NULL::uuid AS weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function reconciliations_staging_update_v1(p_reconciliation_id uuid, p_column text DEFAULT NULL::text, p_column_value boolean DEFAULT NULL::boolean, p_verified boolean DEFAULT NULL::boolean, p_company_id uuid DEFAULT NULL::uuid, p_organization_id uuid DEFAULT NULL::uuid, p_user_id uuid DEFAULT NULL::uuid, p_publish_event boolean DEFAULT true) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_col text;
  v_company_id      uuid;
  v_document_id     uuid;
  v_transaction_id  uuid;

  affected_docs     boolean := false;
  affected_tx       boolean := false;

  v_stage_found     boolean := false;
  v_stage_updated   int := 0;
  v_stage_toggle_upd int := 0;
  v_recon_upd       int := 0;

  j_result jsonb := jsonb_build_object('ok', true);
BEGIN
  /* 0) Resolve the canonical reconciliation row (for link discovery & verified write) */
  SELECT r.company_id, r.document_id, r.transaction_id
    INTO v_company_id, v_document_id, v_transaction_id
  FROM public.reconciliations r
  WHERE r.reconciliation_id = p_reconciliation_id;

  IF NOT FOUND THEN
    RETURN jsonb_build_object('ok', false, 'error', 'not_found');
  END IF;

  /* 1) Optional: toggle one of booked|archived|flagged on staging */
  IF p_column IS NOT NULL THEN
    v_col := CASE lower(p_column)
               WHEN 'booked'   THEN 'booked'
               WHEN 'archived' THEN 'archived'
               WHEN 'flagged'  THEN 'flagged'
               ELSE NULL
             END;

    IF v_col IS NULL THEN
      RETURN jsonb_build_object(
        'ok', false,
        'error', 'invalid_column',
        'allowed', jsonb_build_array('booked','archived','flagged')
      );
    END IF;

    IF p_column_value IS NULL THEN
      RETURN jsonb_build_object(
        'ok', false,
        'error', 'missing_value'
      );
    END IF;

    EXECUTE format(
      'UPDATE public.reconciliations_staging
          SET %I = $1
        WHERE reconciliation_id = $2',
      v_col
    )
    USING p_column_value, p_reconciliation_id;

    GET DIAGNOSTICS v_stage_toggle_upd = ROW_COUNT;

    IF v_stage_toggle_upd = 0 THEN
      -- Staging row not found; surface as internal to match your API’s behavior expectations
      RETURN jsonb_build_object('ok', false, 'error', 'internal', 'message', 'staging_row_not_found');
    END IF;

    j_result := j_result
      || jsonb_build_object(
          'reconciliation_id', p_reconciliation_id::text,
          'column', v_col,
          'value', p_column_value
         );
  END IF;

  /* 2) Optional: verified cascade */
  IF p_verified IS NOT NULL THEN
    -- If setting verified TRUE, must have at least one linked record
    IF p_verified IS TRUE AND v_document_id IS NULL AND v_transaction_id IS NULL THEN
      RETURN jsonb_build_object('ok', false, 'error', 'no_linked_records');
    END IF;

    -- documents_v2
    IF v_document_id IS NOT NULL THEN
      UPDATE public.documents_v2 d
         SET reconciled        = p_verified,
             reconciliation_id = CASE WHEN p_verified THEN p_reconciliation_id ELSE NULL END
       WHERE d.document_id = v_document_id;
      affected_docs := FOUND;
    END IF;

    -- transactions
    IF v_transaction_id IS NOT NULL THEN
      UPDATE public.transactions t
         SET reconciled        = p_verified,
             reconciliation_id = CASE WHEN p_verified THEN p_reconciliation_id ELSE NULL END
       WHERE t.transaction_id = v_transaction_id;
      affected_tx := FOUND;
    END IF;

    -- staging.verified
    UPDATE public.reconciliations_staging rs
       SET verified = p_verified
     WHERE rs.reconciliation_id = p_reconciliation_id;
    GET DIAGNOSTICS v_stage_updated = ROW_COUNT;

    IF v_stage_updated = 0 THEN
      RETURN jsonb_build_object('ok', false, 'error', 'internal', 'message', 'staging_row_not_found');
    END IF;

    -- canonical reconciliations.verified (you asked for this)
    UPDATE public.reconciliations r
       SET verified = p_verified
     WHERE r.reconciliation_id = p_reconciliation_id;
    GET DIAGNOSTICS v_recon_upd = ROW_COUNT;

    j_result := j_result
      || jsonb_build_object(
            'verified', p_verified,
            'affected', jsonb_build_object(
              'documents_v2', affected_docs,
              'transactions', affected_tx
            )
         );
  END IF;

  -- If neither branch executed, treat as bad request
  IF p_column IS NULL AND p_verified IS NULL THEN
    RETURN jsonb_build_object('ok', false, 'error', 'no_op', 'message', 'nothing to update');
  END IF;

  RETURN j_result;

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'ok', false,
    'error', 'internal',
    'message', SQLERRM
  );
END;
$$;

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

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

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

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

create or replace function reconciliations_weight_registry_upsert_v2(p_weight_key text, p_weight_value numeric, p_company_id uuid, p_merchant_id text DEFAULT NULL::text, p_active boolean DEFAULT true) returns reconciliations_weight_registry
    language plpgsql
as
$$
DECLARE
  v_row public.reconciliations_weight_registry;
  v_exists boolean := false;
  v_merchant text;
BEGIN
  -- Normalize merchant id (empty -> NULL)
  v_merchant := NULLIF(btrim(p_merchant_id), '');

  -- 1) Safety: company_id is required for frontend calls
  IF p_company_id IS NULL THEN
    RAISE EXCEPTION 'company_id is required for this operation'
      USING ERRCODE = '22023';
  END IF;

  -- 2) Safety: never allow writes to the DEFAULT registry (company_id NULL AND merchant_id NULL)
  --    (Our requirement above already prevents this, but keep the guard explicit.)
  IF p_company_id IS NULL AND v_merchant IS NULL THEN
    RAISE EXCEPTION 'default registry rows are read-only'
      USING ERRCODE = '42501';
  END IF;

  -- 3) Validate key and value
  IF p_weight_key NOT IN ('date','invoice_number','order_number','merchant_id','amount_total','currency','customer_name') THEN
    RAISE EXCEPTION
      'weight_key must be one of (date, invoice_number, order_number, merchant_id, amount_total, currency, customer_name). Got: %',
      p_weight_key USING ERRCODE = '22023';
  END IF;

  IF p_weight_value IS NULL OR p_weight_value < 0 THEN
    RAISE EXCEPTION 'weight_value must be non-negative. Got: %', p_weight_value USING ERRCODE = '22003';
  END IF;

  -- 4) Check if the row already exists for this (company, merchant, key)
  SELECT TRUE
    INTO v_exists
  FROM public.reconciliations_weight_registry
  WHERE company_id = p_company_id
    AND ((merchant_id IS NULL AND v_merchant IS NULL) OR merchant_id = v_merchant)
    AND weight_key = p_weight_key
  LIMIT 1;

  -- 5) If caller wants to "remove", we soft-delete (active=false) ONLY if it exists.
  IF p_active IS FALSE THEN
    IF NOT v_exists THEN
      RAISE EXCEPTION 'cannot deactivate: row does not exist for (company_id %, merchant_id %, key %)',
        p_company_id, COALESCE(v_merchant,'NULL'), p_weight_key USING ERRCODE = '02000';
    END IF;

    UPDATE public.reconciliations_weight_registry
       SET active = FALSE
     WHERE company_id = p_company_id
       AND ((merchant_id IS NULL AND v_merchant IS NULL) OR merchant_id = v_merchant)
       AND weight_key = p_weight_key
     RETURNING * INTO v_row;

    RETURN v_row;
  END IF;

  -- 6) Upsert (insert or update) for active=true
  INSERT INTO public.reconciliations_weight_registry (
      company_id, merchant_id, weight_key, weight_value, active
  )
  VALUES (p_company_id, v_merchant, p_weight_key, p_weight_value, TRUE)
  ON CONFLICT (company_id, merchant_id, weight_key)
  DO UPDATE SET weight_value = EXCLUDED.weight_value,
                active       = TRUE
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v13(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v13'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight registry snapshots */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  /* 1) Normalize sources into temp tables + indexes */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,

    public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ) AS benef_core_ns,

    public.norm_text_ns(t.name_consumer) AS consumer_core_ns,

    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    public.norm_core_ns(d.merchant_name)       AS merch_core_ns,
    public.norm_text_ns(d.customer_info::text) AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 2) Candidates via targeted branches */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND d.custinfo_core_ns IS NOT NULL
   AND (
        tx.consumer_core_ns = d.custinfo_core_ns
     OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
     OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND length(tx.tx_iban_ns) >= 15
     AND position(tx.tx_iban_ns in d.info_ns) > 0;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND position(tx.tx_bic_ns in d.info_ns) > 0;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 3) Resolve weights */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  /* 4) Flags + confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
        ( c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns  in c.merch_core_ns) > 0 ))
      OR (c.tx_iban_ns IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_iban_ns in c.info_ns) > 0)
      OR (c.tx_bic_ns  IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_bic_ns  in c.info_ns) > 0)
    ) AS merchant_match,
    (c.consumer_core_ns IS NOT NULL AND c.custinfo_core_ns IS NOT NULL AND
      ( c.consumer_core_ns = c.custinfo_core_ns
        OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
        OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 )) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  /* 5) Best per tx + inserts (with optional chunk limit) */
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT *
  FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),                      -- requires pgcrypto; or use uuid_generate_v4()
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function reconciliations_weight_registry_upsert_v3(p_weight_key text, p_weight_value numeric, p_company_id uuid, p_merchant_id text DEFAULT NULL::text, p_active boolean DEFAULT true) returns reconciliations_weight_registry
    language plpgsql
as
$$
DECLARE
  v_row    public.reconciliations_weight_registry;
  v_mer    text := NULLIF(btrim(p_merchant_id), '');
  v_exists boolean;
BEGIN
  IF p_company_id IS NULL THEN
    RAISE EXCEPTION 'company_id is required' USING ERRCODE='22023';
  END IF;

  IF p_weight_key NOT IN ('date','invoice_number','order_number','merchant_id','amount_total','currency','customer_name') THEN
    RAISE EXCEPTION 'invalid weight_key: %', p_weight_key USING ERRCODE='22023';
  END IF;

  IF p_weight_value IS NULL OR p_weight_value < 0 THEN
    RAISE EXCEPTION 'weight_value must be >= 0. Got %', p_weight_value USING ERRCODE='22003';
  END IF;

  -- Do NOT allow touches to global defaults (company_id NULL AND merchant_id NULL)
  -- Already enforced by requiring p_company_id, but keep explicit guard:
  -- (no-op here since p_company_id is NOT NULL)

  -- If caller wants to deactivate, only allow if the row exists
  IF p_active IS FALSE THEN
    SELECT TRUE INTO v_exists
    FROM public.reconciliations_weight_registry
    WHERE company_id = p_company_id
      AND ((merchant_id IS NULL AND v_mer IS NULL) OR merchant_id = v_mer)
      AND weight_key = p_weight_key
    LIMIT 1;

    IF NOT v_exists THEN
      RAISE EXCEPTION 'cannot deactivate: row does not exist for (company_id %, merchant_id %, key %)',
        p_company_id, COALESCE(v_mer,'NULL'), p_weight_key USING ERRCODE='02000';
    END IF;
  END IF;

  -- Single UPSERT on the real UNIQUE constraint (handles NULL merchant via merchant_id_norm)
  INSERT INTO public.reconciliations_weight_registry (
      company_id, merchant_id, weight_key, weight_value, active
  )
  VALUES (p_company_id, v_mer, p_weight_key, p_weight_value, p_active)
  ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
  DO UPDATE SET
      weight_value = EXCLUDED.weight_value,
      active       = EXCLUDED.active
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v14(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v14'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight registry snapshots */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  /* 1) Normalize sources into temp tables + indexes */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,

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

    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,

    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(d.merchant_name), '') AS merch_core_ns,

    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,

    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 2) Candidates via targeted branches */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND length(tx.tx_iban_ns) >= 15
     AND position(tx.tx_iban_ns in d.info_ns) > 0;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND position(tx.tx_bic_ns in d.info_ns) > 0;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 3) Resolve weights */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  /* 4) Flags + confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
        ( c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns  in c.merch_core_ns) > 0 ))
      OR (c.tx_iban_ns IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_iban_ns in c.info_ns) > 0)
      OR (c.tx_bic_ns  IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_bic_ns  in c.info_ns) > 0)
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  /* 5) Best per tx + inserts (with optional chunk limit) */
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT *
  FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v15(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v15'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight registry snapshots */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  /* 1) Normalize sources into temp tables + indexes */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,

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

    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,

    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    -- FIX: use merchant_name or merchant_info->>'name'
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,

    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,

    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 2) Candidates via targeted branches */
  -- [unchanged candidate tables logic from v14] --

  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND length(tx.tx_iban_ns) >= 15
     AND position(tx.tx_iban_ns in d.info_ns) > 0;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND position(tx.tx_bic_ns in d.info_ns) > 0;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 3) Resolve weights */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  /* 4) Flags + confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
        ( c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns  in c.merch_core_ns) > 0 ))
      OR (c.tx_iban_ns IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_iban_ns in c.info_ns) > 0)
      OR (c.tx_bic_ns  IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_bic_ns  in c.info_ns) > 0)
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  /* 5) Best per tx + inserts (with optional chunk limit) */
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT *
  FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v16(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v16'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight registry snapshots */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  /* 1) Normalize sources into temp tables + indexes */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,

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

    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,

    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

    /* NEW: normalized tx info blob (description + remittance) */
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,

    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,

    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 2) Candidates via targeted branches */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND length(tx.tx_iban_ns) >= 15
     AND position(tx.tx_iban_ns in d.info_ns) > 0;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND position(tx.tx_bic_ns in d.info_ns) > 0;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 3) Resolve weights */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  /* 4) Flags + confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
        ( c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns  in c.merch_core_ns) > 0 ))
      OR (c.tx_iban_ns IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_iban_ns in c.info_ns) > 0)
      OR (c.tx_bic_ns  IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_bic_ns  in c.info_ns) > 0)
      OR (c.merch_core_ns IS NOT NULL AND c.info_tx_ns IS NOT NULL AND position(c.merch_core_ns in c.info_tx_ns) > 0)  -- NEW
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  /* 5) Best per tx + inserts (with optional chunk limit) */
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT *
  FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v17(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v17'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight registry snapshots */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  /* 1) Normalize sources into temp tables + indexes */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,

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

    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,

    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

    /* tx info blob for merchant fallback */
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,

    /* merchant name from field or JSON */
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,

    /* precise seller IBAN/BIC for safe checks */
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,

    /* customer normalized block */
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,

    /* info blob kept for customer fallback */
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 2) Candidates via targeted branches */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 3) Resolve weights */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  /* 4) Flags + confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
        ( c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns  in c.merch_core_ns) > 0 ))
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (c.merch_core_ns IS NOT NULL AND c.info_tx_ns IS NOT NULL AND position(c.merch_core_ns in c.info_tx_ns) > 0)
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  /* 5) Best per tx + inserts (with optional chunk limit) */
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT *
  FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v18(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v18'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight registry snapshots */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  /* 1) Normalize sources into temp tables + indexes */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,

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

    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,

    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

    /* tx info blob for merchant fallback */
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,

    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,

    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,

    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,

    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 2) Candidates via targeted branches */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 3) Resolve weights */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  /* 4) Flags + confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  /* 5) Best per tx + inserts (with optional chunk limit) */
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT *
  FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v19(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v19'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weights snapshot */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  /* 1) Normalize inputs */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,

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

    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,

    /* Use counterparty/beneficiary only. Exclude payer fields. */
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

    /* Tx info blob for fallback name containment */
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,

    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,

    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,

    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,

    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 2) Candidate pairs */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 3) Resolve weights */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  /* 4) Flags and confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  /* 5) Best per tx and write-out */
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT *
  FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v20(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v20'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
BEGIN
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT *
  FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function reconciliation_weights_auto_v1(p_company_id uuid DEFAULT NULL::uuid, p_days integer DEFAULT 90, p_min_rows integer DEFAULT 200) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_rows       int := 0;
  v_companies  int := 0;
BEGIN
  -- 1) Aggregate recent match stats
  CREATE TEMP TABLE tmp_base AS
  SELECT r.company_id,
         COUNT(*)                                  AS n,
         SUM(s.date_match::int)                    AS hit_date,
         SUM(s.invoice_match::int)                 AS hit_inv,
         SUM(s.order_match::int)                   AS hit_ord,
         SUM(s.merchant_match::int)                AS hit_mer,
         SUM(s.amount_match::int)                  AS hit_amt,
         SUM(s.currency_match::int)                AS hit_cur,
         SUM(s.customer_match::int)                AS hit_cus
  FROM public.reconciliations_match_scores s
  JOIN public.reconciliations r ON r.reconciliation_id = s.reconciliation_id
  WHERE (p_company_id IS NULL OR r.company_id = p_company_id)
    AND s.created_at >= now() - make_interval(days => p_days)
  GROUP BY r.company_id
  HAVING COUNT(*) >= p_min_rows;

  -- 2) Smoothed hit rates (Beta(2,3))
  CREATE TEMP TABLE tmp_rates AS
  SELECT company_id, n,
         (hit_date+2.0)/(n+5.0) AS r_date,
         (hit_inv +2.0)/(n+5.0) AS r_inv,
         (hit_ord +2.0)/(n+5.0) AS r_ord,
         (hit_mer +2.0)/(n+5.0) AS r_mer,
         (hit_amt +2.0)/(n+5.0) AS r_amt,
         (hit_cur +2.0)/(n+5.0) AS r_cur,
         (hit_cus +2.0)/(n+5.0) AS r_cus
  FROM tmp_base;

  -- 3) Desired weights per company
  CREATE TEMP TABLE tmp_desired_weights AS
  SELECT company_id, weight_key, weight_value
  FROM (
    SELECT company_id,'date'           ::text AS weight_key, ROUND(GREATEST(0.50, LEAST(1.00, 0.50 + 0.50*r_date))::numeric,2) AS weight_value FROM tmp_rates
    UNION ALL
    SELECT company_id,'invoice_number' ::text, ROUND(GREATEST(1.00, LEAST(1.80, 1.00 + 0.80*r_inv ))::numeric,2) FROM tmp_rates
    UNION ALL
    SELECT company_id,'order_number'   ::text, ROUND(GREATEST(0.40, LEAST(1.00, 0.40 + 0.60*r_ord ))::numeric,2) FROM tmp_rates
    UNION ALL
    SELECT company_id,'merchant_id'    ::text, ROUND(GREATEST(1.00, LEAST(1.50, 1.00 + 0.50*r_mer ))::numeric,2) FROM tmp_rates
    UNION ALL
    SELECT company_id,'amount_total'   ::text, ROUND(GREATEST(1.00, LEAST(1.70, 1.00 + 0.70*r_amt ))::numeric,2) FROM tmp_rates
    UNION ALL
    SELECT company_id,'currency'       ::text, ROUND(GREATEST(0.01, LEAST(0.20, 0.02 + 0.18*r_cur))::numeric,2) FROM tmp_rates
    UNION ALL
    SELECT company_id,'customer_name'  ::text, ROUND(GREATEST(0.50, LEAST(1.00, 0.50 + 0.50*r_cus))::numeric,2) FROM tmp_rates
  ) u;

  -- 4) Upsert into registry (uses your unique constraint on generated column)
  INSERT INTO public.reconciliations_weight_registry
    (company_id, merchant_id, weight_key, weight_value, active, created_at)
  SELECT d.company_id, NULL, d.weight_key, d.weight_value, TRUE, now()
  FROM tmp_desired_weights d
  ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
  DO UPDATE SET
    weight_value = EXCLUDED.weight_value,
    active       = TRUE,
    created_at   = now();

  GET DIAGNOSTICS v_rows = ROW_COUNT;
  SELECT COUNT(DISTINCT company_id) INTO v_companies FROM tmp_desired_weights;

  RETURN jsonb_build_object(
    'status','ok',
    'companies_considered', (SELECT COUNT(*) FROM tmp_base),
    'companies_updated',    v_companies,
    'rows_upserted',        COALESCE(v_rows,0),
    'lookback_days',        p_days,
    'min_rows',             p_min_rows,
    'filtered_company',     p_company_id::text
  );
END;
$$;

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

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

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

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

create or replace function reconciliation_weights_auto_v2(p_company_id uuid DEFAULT NULL::uuid, p_days integer DEFAULT 90, p_min_rows integer DEFAULT 200) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_rows      int := 0;
  v_companies int := 0;
BEGIN
  -- 1) Aggregate recent match stats per company
  CREATE TEMP TABLE tmp_base AS
  SELECT r.company_id,
         COUNT(*)                                  AS n,
         SUM(s.date_match::int)                    AS hit_date,
         SUM(s.invoice_match::int)                 AS hit_inv,
         SUM(s.order_match::int)                   AS hit_ord,
         SUM(s.merchant_match::int)                AS hit_mer,
         SUM(s.amount_match::int)                  AS hit_amt,
         SUM(s.currency_match::int)                AS hit_cur,
         SUM(s.customer_match::int)                AS hit_cus
  FROM public.reconciliations_match_scores s
  JOIN public.reconciliations r ON r.reconciliation_id = s.reconciliation_id
  WHERE (p_company_id IS NULL OR r.company_id = p_company_id)
    AND s.created_at >= now() - make_interval(days => p_days)
  GROUP BY r.company_id
  HAVING COUNT(*) >= p_min_rows;

  -- 2) Smoothed rates (Beta(2,3)) to avoid extremes on small samples
  CREATE TEMP TABLE tmp_rates AS
  SELECT company_id, n,
         (hit_date+2.0)/(n+5.0) AS r_date,
         (hit_inv +2.0)/(n+5.0) AS r_inv,
         (hit_ord +2.0)/(n+5.0) AS r_ord,
         (hit_mer +2.0)/(n+5.0) AS r_mer,
         (hit_amt +2.0)/(n+5.0) AS r_amt,
         (hit_cur +2.0)/(n+5.0) AS r_cur,
         (hit_cus +2.0)/(n+5.0) AS r_cus
  FROM tmp_base;

  -- 3) Desired weights with anchored bands (importance-first), capped at 2.00
  -- Bands (min..max):
  --   invoice: 1.6..1.8   order: 1.0..1.5   amount: 1.2..1.7
  --   merchant: 1.0..1.4  date: 0.8..1.2    customer: 0.4..0.8
  --   currency: 0.05..0.20
  CREATE TEMP TABLE tmp_desired_weights AS
  SELECT company_id, weight_key, weight_value
  FROM (
    SELECT company_id,'date'::text            AS weight_key,
           ROUND(LEAST(2.00, GREATEST(0.80, LEAST(1.20, 0.80 + 0.40*r_date)))::numeric, 2) AS weight_value
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'invoice_number'::text,
           ROUND(LEAST(2.00, GREATEST(1.60, LEAST(1.80, 1.60 + 0.20*r_inv)))::numeric, 2)
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'order_number'::text,
           ROUND(LEAST(2.00, GREATEST(1.00, LEAST(1.50, 1.00 + 0.50*r_ord)))::numeric, 2)
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'merchant_id'::text,
           ROUND(LEAST(2.00, GREATEST(1.00, LEAST(1.40, 1.00 + 0.40*r_mer)))::numeric, 2)
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'amount_total'::text,
           ROUND(LEAST(2.00, GREATEST(1.20, LEAST(1.70, 1.20 + 0.50*r_amt)))::numeric, 2)
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'currency'::text,
           ROUND(LEAST(2.00, GREATEST(0.05, LEAST(0.20, 0.05 + 0.15*r_cur)))::numeric, 2)
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'customer_name'::text,
           ROUND(LEAST(2.00, GREATEST(0.40, LEAST(0.80, 0.40 + 0.40*r_cus)))::numeric, 2)
    FROM tmp_rates
  ) u;

  -- 4) Upsert (company-level weights; merchant_id NULL)
  INSERT INTO public.reconciliations_weight_registry
    (company_id, merchant_id, weight_key, weight_value, active, created_at)
  SELECT d.company_id, NULL, d.weight_key, d.weight_value, TRUE, now()
  FROM tmp_desired_weights d
  ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
  DO UPDATE SET
    weight_value = EXCLUDED.weight_value,
    active       = TRUE,
    created_at   = now();

  GET DIAGNOSTICS v_rows = ROW_COUNT;
  SELECT COUNT(DISTINCT company_id) INTO v_companies FROM tmp_desired_weights;

  RETURN jsonb_build_object(
    'status','ok',
    'companies_considered', (SELECT COUNT(*) FROM tmp_base),
    'companies_updated',    v_companies,
    'rows_upserted',        COALESCE(v_rows,0),
    'lookback_days',        p_days,
    'min_rows',             p_min_rows,
    'filtered_company',     p_company_id::text
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v21(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v21'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
BEGIN
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* representative doc per (company, dedup_serialized) if duplicates disallowed */
  CREATE TEMP TABLE _docs_rep AS
  SELECT DISTINCT ON (company_id_uuid, dedup_serialized)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM _docs
  ORDER BY company_id_uuid, dedup_serialized, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  /* 2) Candidate pairs */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v22(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v22'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
BEGIN
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  -- representative doc per (company, dedup_key) when duplicates are disallowed
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT
      d.*,
      COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v23(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v23'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;     -- total ms for this function run (so far) 
  v_ms_each numeric := 0;    -- per-row ms written to reconciliations.processing_time
BEGIN
  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  -- representative doc per (company, dedup_key) when duplicates are disallowed
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT
      d.*,
      COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- INSERT reconciliations, capture inserted IDs, compute ms-per-row, update processing_time
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)   -- or ::bigint if the column is integer
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  -- write match score rows for inserted reconciliations
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v24(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v24'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;     -- total ms for this function run (so far)
  v_ms_each numeric := 0;    -- per-row ms written to reconciliations.processing_time
  v_run_id uuid;             -- logging run id
BEGIN
  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => 'athena_reconcile_general_v24',
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id        => v_run_id,
    p_action        => 'registry_loaded',
    p_status        => 'ok',
    p_company_id    => p_company_id,
    p_step_ms       => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id      => v_run_id,
    p_action      => 'normalized',
    p_status      => 'ok',
    p_company_id  => p_company_id,
    p_context     => jsonb_build_object(
                      'tx_rows',   (SELECT COUNT(*) FROM _tx),
                      'doc_rows',  (SELECT COUNT(*) FROM _docs),
                      'docs_used', NULL
                    )
  );

  -- representative doc per (company, dedup_key) when duplicates are disallowed
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT
      d.*,
      COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id      => v_run_id,
    p_action      => 'docs_selected',
    p_status      => 'ok',
    p_company_id  => p_company_id,
    p_context     => jsonb_build_object('docs_used', (SELECT COUNT(*) FROM _docs_use),
                                        'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id        => v_run_id,
    p_action        => 'candidates_built',
    p_status        => 'ok',
    p_company_id    => p_company_id,
    p_rows_affected => v_candidates,
    p_context       => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- INSERT reconciliations, capture inserted IDs, compute ms-per-row, update processing_time
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id        => v_run_id,
    p_action        => 'reconciliations_inserted',
    p_status        => 'ok',
    p_company_id    => p_company_id,
    p_rows_affected => v_rows,
    p_context       => jsonb_build_object('per_row_ms', v_ms_each,
                                          'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- write match score rows for inserted reconciliations
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  PERFORM public.reconciliations_log_end(
    p_run_id        => v_run_id,
    p_status        => 'ok',
    p_message       => NULL,
    p_processing_ms => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context       => jsonb_build_object('inserted', v_rows, 'candidates', v_candidates)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id        => v_run_id,
    p_status        => 'error',
    p_message       => SQLERRM,
    p_processing_ms => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context       => jsonb_build_object('inserted_so_far', v_rows, 'candidates', v_candidates)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function reconciliations_log_start(p_function_version text, p_company_id uuid DEFAULT NULL::uuid, p_context jsonb DEFAULT '{}'::jsonb) returns uuid
    language plpgsql
as
$$
DECLARE v_run_id uuid := gen_random_uuid();
BEGIN
  PERFORM public.reconciliations_log_add(
    p_run_id            => v_run_id,
    p_function_version  => p_function_version,
    p_action            => 'run_start',
    p_status            => 'ok',
    p_target            => 'function',
    p_message           => NULL,
    p_company_id        => p_company_id,
    p_context           => p_context
  );
  RETURN v_run_id;
END;
$$;

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

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

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

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

create or replace function reconciliations_log_end(p_run_id uuid, p_function_version text, p_status text, p_message text DEFAULT NULL::text, p_processing_ms bigint DEFAULT NULL::bigint, p_context jsonb DEFAULT '{}'::jsonb) returns void
    language sql
as
$$
  SELECT public.reconciliations_log_add(
    p_run_id, p_function_version,
    'run_end', p_status,
    'function', p_message,
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, p_processing_ms, p_context
  );
$$;

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

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

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

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

create or replace function athena_reconcile_general_v25(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v25'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
BEGIN
  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- INSERT reconciliations
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'reconciliations_inserted'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_rows::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('per_row_ms', v_ms_each, 'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- write match scores
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'ok'::text,
    p_message           => NULL::text,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'error'::text,
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted_so_far', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function reconciliations_log_step(p_run_id uuid, p_function_version text, p_action text, p_status text DEFAULT 'ok'::text, p_target text DEFAULT NULL::text, p_message text DEFAULT NULL::text, p_company_id uuid DEFAULT NULL::uuid, p_transaction_id uuid DEFAULT NULL::uuid, p_document_id uuid DEFAULT NULL::uuid, p_reconciliation_id uuid DEFAULT NULL::uuid, p_rows_affected integer DEFAULT NULL::integer, p_confidence numeric DEFAULT NULL::numeric, p_step_ms bigint DEFAULT NULL::bigint, p_processing_ms bigint DEFAULT NULL::bigint, p_context jsonb DEFAULT '{}'::jsonb) returns void
    language sql
as
$$
  SELECT public.reconciliations_log_add(
    p_run_id, p_function_version, p_action, p_status, p_target, p_message,
    p_company_id, p_transaction_id, p_document_id, p_reconciliation_id,
    p_rows_affected, p_confidence, p_step_ms, p_processing_ms, p_context
  );
$$;

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

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

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

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

create or replace function reconciliations_staging_update_v2(p_reconciliation_id uuid, p_column text DEFAULT NULL::text, p_column_value boolean DEFAULT NULL::boolean, p_verified boolean DEFAULT NULL::boolean, p_company_id uuid DEFAULT NULL::uuid, p_organization_id uuid DEFAULT NULL::uuid, p_user_id uuid DEFAULT NULL::uuid, p_publish_event boolean DEFAULT true) returns jsonb
    language plpgsql
as
$$
DECLARE
  -- timing + logging
  v_t0           timestamptz := clock_timestamp();
  v_run_id       uuid;

  v_col              text;
  v_company_id       uuid;
  v_document_id      uuid;
  v_transaction_id   uuid;

  affected_docs      boolean := false;
  affected_tx        boolean := false;

  v_stage_updated     int := 0;
  v_stage_toggle_upd  int := 0;
  v_recon_upd         int := 0;
  v_docs_upd          int := 0;
  v_tx_upd            int := 0;

  j_result jsonb := jsonb_build_object('ok', true);
BEGIN
  /* 0) Start logging */
  v_run_id := public.reconciliations_log_start(
    p_function_version => 'reconciliations_staging_update_v2',
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'reconciliation_id', p_reconciliation_id,
                             'column',            p_column,
                             'column_value',      p_column_value,
                             'verified',          p_verified,
                             'organization_id',   p_organization_id,
                             'user_id',           p_user_id,
                             'publish_event',     p_publish_event
                           )
                         )
  );

  /* 1) Resolve canonical reconciliation row (for links & verified cascade) */
  SELECT r.company_id, r.document_id, r.transaction_id
    INTO v_company_id, v_document_id, v_transaction_id
  FROM public.reconciliations r
  WHERE r.reconciliation_id = p_reconciliation_id;

  IF NOT FOUND THEN
    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'resolve_reconciliation',
      p_status            => 'error',
      p_target            => 'reconciliations_staging',
      p_message           => 'not_found',
      p_company_id        => p_company_id,
      p_transaction_id    => NULL::uuid,
      p_document_id       => NULL::uuid,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => NULL::int,
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => '{}'::jsonb
    );

    PERFORM public.reconciliations_log_end(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_status            => 'error',
      p_message           => 'not_found',
      p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_context           => jsonb_build_object('reconciliation_id', p_reconciliation_id)
    );

    RETURN jsonb_build_object('ok', false, 'error', 'not_found');
  END IF;

  /* 2) Optional: toggle one of booked|archived|flagged on staging */
  IF p_column IS NOT NULL THEN
    v_col := CASE lower(p_column)
               WHEN 'booked'   THEN 'booked'
               WHEN 'archived' THEN 'archived'
               WHEN 'flagged'  THEN 'flagged'
               ELSE NULL
             END;

    IF v_col IS NULL THEN
      PERFORM public.reconciliations_log_step(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_action            => 'toggle_column',
        p_status            => 'error',
        p_target            => 'reconciliations_staging',
        p_message           => 'invalid_column',
        p_company_id        => v_company_id,
        p_transaction_id    => v_transaction_id,
        p_document_id       => v_document_id,
        p_reconciliation_id => p_reconciliation_id,
        p_rows_affected     => NULL::int,
        p_confidence        => NULL::numeric,
        p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_processing_ms     => NULL::bigint,
        p_context           => jsonb_build_object('provided', p_column)
      );

      PERFORM public.reconciliations_log_end(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_status            => 'error',
        p_message           => 'invalid_column',
        p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_context           => jsonb_build_object('allowed', jsonb_build_array('booked','archived','flagged'))
      );

      RETURN jsonb_build_object(
        'ok', false,
        'error', 'invalid_column',
        'allowed', jsonb_build_array('booked','archived','flagged')
      );
    END IF;

    IF p_column_value IS NULL THEN
      PERFORM public.reconciliations_log_step(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_action            => 'toggle_column',
        p_status            => 'error',
        p_target            => 'reconciliations_staging',
        p_message           => 'missing_value',
        p_company_id        => v_company_id,
        p_transaction_id    => v_transaction_id,
        p_document_id       => v_document_id,
        p_reconciliation_id => p_reconciliation_id,
        p_rows_affected     => NULL::int,
        p_confidence        => NULL::numeric,
        p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_processing_ms     => NULL::bigint,
        p_context           => jsonb_build_object('column', v_col)
      );

      PERFORM public.reconciliations_log_end(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_status            => 'error',
        p_message           => 'missing_value',
        p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_context           => jsonb_build_object('column', v_col)
      );

      RETURN jsonb_build_object('ok', false, 'error', 'missing_value');
    END IF;

    EXECUTE format(
      'UPDATE public.reconciliations_staging
          SET %I = $1
        WHERE reconciliation_id = $2',
      v_col
    )
    USING p_column_value, p_reconciliation_id;

    GET DIAGNOSTICS v_stage_toggle_upd = ROW_COUNT;

    IF v_stage_toggle_upd = 0 THEN
      PERFORM public.reconciliations_log_step(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_action            => 'toggle_column',
        p_status            => 'error',
        p_target            => 'reconciliations_staging',
        p_message           => 'staging_row_not_found',
        p_company_id        => v_company_id,
        p_transaction_id    => v_transaction_id,
        p_document_id       => v_document_id,
        p_reconciliation_id => p_reconciliation_id,
        p_rows_affected     => 0,
        p_confidence        => NULL::numeric,
        p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_processing_ms     => NULL::bigint,
        p_context           => jsonb_build_object('column', v_col, 'value', p_column_value)
      );

      PERFORM public.reconciliations_log_end(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_status            => 'error',
        p_message           => 'staging_row_not_found',
        p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_context           => jsonb_build_object('reconciliation_id', p_reconciliation_id)
      );

      RETURN jsonb_build_object('ok', false, 'error', 'internal', 'message', 'staging_row_not_found');
    END IF;

    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'toggle_column',
      p_status            => 'ok',
      p_target            => 'reconciliations_staging',
      p_message           => NULL::text,
      p_company_id        => v_company_id,
      p_transaction_id    => v_transaction_id,
      p_document_id       => v_document_id,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => v_stage_toggle_upd,
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => jsonb_build_object('column', v_col, 'value', p_column_value)
    );

    j_result := j_result
      || jsonb_build_object(
           'reconciliation_id', p_reconciliation_id::text,
           'column', v_col,
           'value', p_column_value
         );
  END IF;

  /* 3) Optional: verified (+ separate logs for flag vs. cascade) */
  IF p_verified IS NOT NULL THEN
    -- Precheck: verifying TRUE but no links
    IF p_verified IS TRUE AND v_document_id IS NULL AND v_transaction_id IS NULL THEN
      PERFORM public.reconciliations_log_step(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_action            => 'verify_precheck',
        p_status            => 'error',
        p_target            => 'reconciliations',
        p_message           => 'no_linked_records',
        p_company_id        => v_company_id,
        p_transaction_id    => v_transaction_id,
        p_document_id       => v_document_id,
        p_reconciliation_id => p_reconciliation_id,
        p_rows_affected     => 0,
        p_confidence        => NULL::numeric,
        p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_processing_ms     => NULL::bigint,
        p_context           => jsonb_build_object('verified', p_verified)
      );

      PERFORM public.reconciliations_log_end(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_status            => 'error',
        p_message           => 'no_linked_records',
        p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_context           => jsonb_build_object('reconciliation_id', p_reconciliation_id)
      );

      RETURN jsonb_build_object('ok', false, 'error', 'no_linked_records');
    END IF;

    -- 3a) Flip verify flags on canonical + staging (VERIFY FLAG STEP)
    UPDATE public.reconciliations r
       SET verified = p_verified
     WHERE r.reconciliation_id = p_reconciliation_id;
    GET DIAGNOSTICS v_recon_upd = ROW_COUNT;

    UPDATE public.reconciliations_staging rs
       SET verified = p_verified
     WHERE rs.reconciliation_id = p_reconciliation_id;
    GET DIAGNOSTICS v_stage_updated = ROW_COUNT;

    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'verify_flag',
      p_status            => 'ok',
      p_target            => 'reconciliations+staging',
      p_message           => NULL::text,
      p_company_id        => v_company_id,
      p_transaction_id    => v_transaction_id,
      p_document_id       => v_document_id,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => COALESCE(v_recon_upd,0) + COALESCE(v_stage_updated,0),
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => jsonb_build_object(
                               'verified', p_verified,
                               'reconciliations_rows', v_recon_upd,
                               'staging_rows', v_stage_updated
                             )
    );

    -- 3b) Cascade to documents (CASCADE DOCS STEP)
    IF v_document_id IS NOT NULL THEN
      UPDATE public.documents_v2 d
         SET reconciled        = p_verified,
             reconciliation_id = CASE WHEN p_verified THEN p_reconciliation_id ELSE NULL END
       WHERE d.document_id = v_document_id;
      GET DIAGNOSTICS v_docs_upd = ROW_COUNT;
      affected_docs := (v_docs_upd > 0);
    END IF;

    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'cascade_documents',
      p_status            => CASE WHEN v_document_id IS NULL THEN 'noop' ELSE 'ok' END,
      p_target            => 'documents_v2',
      p_message           => NULL::text,
      p_company_id        => v_company_id,
      p_transaction_id    => v_transaction_id,
      p_document_id       => v_document_id,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => COALESCE(v_docs_upd,0),
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => jsonb_build_object('verified', p_verified)
    );

    -- 3c) Cascade to transactions (CASCADE TX STEP)
    IF v_transaction_id IS NOT NULL THEN
      UPDATE public.transactions t
         SET reconciled        = p_verified,
             reconciliation_id = CASE WHEN p_verified THEN p_reconciliation_id ELSE NULL END
       WHERE t.transaction_id = v_transaction_id;
      GET DIAGNOSTICS v_tx_upd = ROW_COUNT;
      affected_tx := (v_tx_upd > 0);
    END IF;

    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'cascade_transactions',
      p_status            => CASE WHEN v_transaction_id IS NULL THEN 'noop' ELSE 'ok' END,
      p_target            => 'transactions',
      p_message           => NULL::text,
      p_company_id        => v_company_id,
      p_transaction_id    => v_transaction_id,
      p_document_id       => v_document_id,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => COALESCE(v_tx_upd,0),
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => jsonb_build_object('verified', p_verified)
    );

    -- Result payload enrichment
    j_result := j_result
      || jsonb_build_object(
           'verified', p_verified,
           'affected', jsonb_build_object(
             'documents_v2', affected_docs,
             'transactions', affected_tx
           ),
           'rows', jsonb_build_object(
             'reconciliations', v_recon_upd,
             'staging',         v_stage_updated,
             'documents_v2',    v_docs_upd,
             'transactions',    v_tx_upd
           )
         );
  END IF;

  -- If neither branch executed, bad request
  IF p_column IS NULL AND p_verified IS NULL THEN
    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'no_op',
      p_status            => 'error',
      p_target            => 'reconciliations_staging',
      p_message           => 'nothing to update',
      p_company_id        => v_company_id,
      p_transaction_id    => v_transaction_id,
      p_document_id       => v_document_id,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => NULL::int,
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => '{}'::jsonb
    );

    PERFORM public.reconciliations_log_end(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_status            => 'error',
      p_message           => 'nothing to update',
      p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_context           => jsonb_build_object('reconciliation_id', p_reconciliation_id)
    );

    RETURN jsonb_build_object('ok', false, 'error', 'no_op', 'message', 'nothing to update');
  END IF;

  -- End logging (success)
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_staging_update_v2',
    p_status            => 'ok',
    p_message           => NULL,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_context           => j_result
  );

  RETURN j_result;

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_staging_update_v2',
    p_status            => 'error',
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_context           => jsonb_build_object(
                             'reconciliation_id', p_reconciliation_id,
                             'column', p_column,
                             'verified', p_verified
                           )
  );

  RETURN jsonb_build_object(
    'ok', false,
    'error', 'internal',
    'message', SQLERRM
  );
END;
$$;

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

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

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

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

create or replace function reconciliations_log_add(p_run_id uuid, p_function_version text, p_action text, p_status text, p_target text DEFAULT NULL::text, p_message text DEFAULT NULL::text, p_company_id uuid DEFAULT NULL::uuid, p_transaction_id uuid DEFAULT NULL::uuid, p_document_id uuid DEFAULT NULL::uuid, p_reconciliation_id uuid DEFAULT NULL::uuid, p_rows_affected integer DEFAULT NULL::integer, p_confidence numeric DEFAULT NULL::numeric, p_step_ms bigint DEFAULT NULL::bigint, p_processing_ms bigint DEFAULT NULL::bigint, p_context jsonb DEFAULT '{}'::jsonb) returns void
    language plpgsql
as
$$
BEGIN
  INSERT INTO public.reconciliations_logs (
    reconciliation_log_id, run_id, created_at, function_version,
    action, status, target, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    gen_random_uuid(), p_run_id, now(), p_function_version,
    p_action, p_status, p_target, p_message,
    p_company_id, p_transaction_id, p_document_id, p_reconciliation_id,
    p_rows_affected, p_confidence, p_step_ms, p_processing_ms,
    COALESCE(p_context, '{}'::jsonb)
  );
END;
$$;

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

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

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

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

create or replace function reconciliations_weight_registry_upsert_v4(p_weight_key text, p_weight_value numeric, p_company_id uuid, p_merchant_id text DEFAULT NULL::text, p_active boolean DEFAULT true) returns reconciliations_weight_registry
    language plpgsql
as
$$
DECLARE
  v_row       public.reconciliations_weight_registry;
  v_mer       text := NULLIF(btrim(p_merchant_id), '');
  v_exists    boolean;
  v_t0        timestamptz := clock_timestamp();
  v_run_id    uuid;
BEGIN
  -- start logging immediately (even if inputs are invalid, EXCEPTION will close it)
  v_run_id := public.reconciliations_log_start(
    p_function_version => 'reconciliations_weight_registry_upsert_v4',
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'weight_key',    p_weight_key,
                             'weight_value',  p_weight_value,
                             'company_id',    p_company_id,
                             'merchant_id_in',p_merchant_id,
                             'active',        p_active
                           )
                         )
  );

  -- validations (same behavior as before)
  IF p_company_id IS NULL THEN
    RAISE EXCEPTION 'company_id is required' USING ERRCODE='22023';
  END IF;

  IF p_weight_key NOT IN ('date','invoice_number','order_number','merchant_id','amount_total','currency','customer_name') THEN
    RAISE EXCEPTION 'invalid weight_key: %', p_weight_key USING ERRCODE='22023';
  END IF;

  IF p_weight_value IS NULL OR p_weight_value < 0 THEN
    RAISE EXCEPTION 'weight_value must be >= 0. Got %', p_weight_value USING ERRCODE='22003';
  END IF;

  -- log successful validation
  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_weight_registry_upsert_v4',
    p_action            => 'validated',
    p_status            => 'ok',
    p_target            => 'reconciliations_weight_registry',
    p_message           => NULL,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('merchant_norm', v_mer)
  );

  -- If caller wants to deactivate, only allow if row exists (preserves previous behavior)
  IF p_active IS FALSE THEN
    SELECT TRUE INTO v_exists
    FROM public.reconciliations_weight_registry
    WHERE company_id = p_company_id
      AND ((merchant_id IS NULL AND v_mer IS NULL) OR merchant_id = v_mer)
      AND weight_key = p_weight_key
    LIMIT 1;

    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_weight_registry_upsert_v4',
      p_action            => 'deactivate_check',
      p_status            => CASE WHEN v_exists THEN 'ok' ELSE 'error' END,
      p_target            => 'reconciliations_weight_registry',
      p_message           => CASE WHEN v_exists THEN NULL ELSE 'row_not_found_for_deactivate' END,
      p_company_id        => p_company_id,
      p_context           => jsonb_build_object('merchant_norm', v_mer, 'weight_key', p_weight_key)
    );

    IF NOT v_exists THEN
      RAISE EXCEPTION 'cannot deactivate: row does not exist for (company_id %, merchant_id %, key %)',
        p_company_id, COALESCE(v_mer,'NULL'), p_weight_key USING ERRCODE='02000';
    END IF;
  END IF;

  -- Single UPSERT against unique constraint (same as v3)
  INSERT INTO public.reconciliations_weight_registry (
      company_id, merchant_id, weight_key, weight_value, active
  )
  VALUES (p_company_id, v_mer, p_weight_key, p_weight_value, p_active)
  ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
  DO UPDATE SET
      weight_value = EXCLUDED.weight_value,
      active       = EXCLUDED.active
  RETURNING * INTO v_row;

  -- log the upsert
  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_weight_registry_upsert_v4',
    p_action            => 'upsert',
    p_status            => 'ok',
    p_target            => 'reconciliations_weight_registry',
    p_message           => NULL,
    p_company_id        => p_company_id,
    p_rows_affected     => 1,
    p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_context           => jsonb_build_object(
                             'key',     v_row.weight_key,
                             'value',   v_row.weight_value,
                             'active',  v_row.active,
                             'merchant_id', v_row.merchant_id
                           )
  );

  -- end logging (success)
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_weight_registry_upsert_v4',
    p_status            => 'ok',
    p_message           => NULL,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_context           => jsonb_build_object(
                             'company_id', p_company_id,
                             'merchant_norm', v_mer,
                             'weight_key', p_weight_key
                           )
  );

  RETURN v_row;

EXCEPTION WHEN OTHERS THEN
  -- close the log and rethrow to preserve previous error behavior
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_weight_registry_upsert_v4',
    p_status            => 'error',
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_context           => jsonb_build_object(
                             'args', jsonb_build_object(
                               'weight_key',    p_weight_key,
                               'weight_value',  p_weight_value,
                               'company_id',    p_company_id,
                               'merchant_id_in',p_merchant_id,
                               'active',        p_active
                             )
                           )
  );
  RAISE;
END;
$$;

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

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

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

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

create or replace function delete_ponto_token(p_user_id integer) returns void
    language plpgsql
as
$$
BEGIN
    DELETE FROM ponto_tokens
    WHERE user_id = p_user_id;
END;
$$;

alter function delete_ponto_token(unknown) owner to postgres;

grant execute on function delete_ponto_token(unknown) to anon;

grant execute on function delete_ponto_token(unknown) to authenticated;

grant execute on function delete_ponto_token(unknown) to service_role;

create or replace function delete_ponto_user(p_user_id text) returns void
    language plpgsql
as
$$ BEGIN -- Delete from tokens
DELETE FROM ponto_tokens
WHERE user_id::TEXT = p_user_id;
-- Delete from user info
DELETE FROM ponto_user_info
WHERE user_id::TEXT = p_user_id;
-- Delete from company mapping
DELETE FROM ponto_user_company_mapping
WHERE user_id::TEXT = p_user_id;
-- Delete from onboarding details
DELETE FROM ponto_onboarding_details
WHERE partner_reference = p_user_id;
-- Delete from account
UPDATE ponto_account
SET revoked = true,
    is_hard_deleted = true,
    deleted_at = EXTRACT(
        EPOCH
        FROM NOW()
    )::BIGINT
WHERE user_id::TEXT = p_user_id;
-- Log success
INSERT INTO public.ponto_logs (
        message,
        status,
        resource,
        user_id,
        action,
        level,
        file
    )
VALUES (
        'Deleted user data from ponto_tokens, ponto_user_info, ponto_user_company_mapping, ponto_onboarding_details, and ponto_account',
        'success',
        'ponto_user',
        p_user_id,
        'delete',
        'info',
        'delete_ponto_user'
    );
EXCEPTION
WHEN OTHERS THEN -- Log failure
INSERT INTO public.ponto_logs (
        message,
        status,
        resource,
        user_id,
        action,
        level,
        file,
        error
    )
VALUES (
        'Failed to delete user data',
        'error',
        'ponto_user',
        p_user_id,
        'delete',
        'error',
        'delete_ponto_user',
        SQLERRM
    );
RAISE;
END;
$$;

alter function delete_ponto_user(unknown) owner to postgres;

grant execute on function delete_ponto_user(unknown) to anon;

grant execute on function delete_ponto_user(unknown) to authenticated;

grant execute on function delete_ponto_user(unknown) to service_role;

create or replace function reconciliations_activity_log_v1(p_run_id uuid, p_company_id uuid, p_transaction_id uuid, p_document_id uuid, p_action text, p_reason text, p_details jsonb DEFAULT '{}'::jsonb) returns uuid
    language plpgsql
as
$$
DECLARE
  v_activity_id uuid := gen_random_uuid();
BEGIN
  INSERT INTO public.reconciliations_activity (
    activity_id, run_id, company_id, transaction_id, document_id, action, reason, details
  )
  VALUES (
    v_activity_id, p_run_id, p_company_id, p_transaction_id, p_document_id, p_action, p_reason, COALESCE(p_details, '{}'::jsonb)
  );
  RETURN v_activity_id;
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v26(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v26'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false, p_log_activity boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
  rec RECORD;
BEGIN
  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates,
                             'log_activity',     p_log_activity
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- activity: transactions/documents with no candidates
  IF p_log_activity THEN
    FOR rec IN
      SELECT
        tx.company_id_txt::uuid AS company_id,
        tx.transaction_id,
        tx.debited_num, tx.invoice_nr_norm, tx.order_id_norm, tx.tx_iban_ns, tx.tx_bic_ns,
        tx.benef_core_ns, tx.consumer_core_ns, tx.currency_norm_tx, tx.completed_date
      FROM _tx tx
      LEFT JOIN _cand c ON c.transaction_id = tx.transaction_id
      WHERE c.transaction_id IS NULL
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id, rec.transaction_id, NULL,
        'considered','no_candidates',
        jsonb_build_object(
          'signals_present', jsonb_build_object(
            'amount',   rec.debited_num IS NOT NULL,
            'invoice',  rec.invoice_nr_norm IS NOT NULL,
            'order',    rec.order_id_norm IS NOT NULL,
            'iban',     rec.tx_iban_ns IS NOT NULL,
            'bic',      rec.tx_bic_ns IS NOT NULL,
            'benef',    rec.benef_core_ns IS NOT NULL,
            'consumer', rec.consumer_core_ns IS NOT NULL,
            'currency', rec.currency_norm_tx IS NOT NULL,
            'date',     rec.completed_date IS NOT NULL
          )
        )
      );
    END LOOP;

    FOR rec IN
      SELECT
        d.company_id_uuid AS company_id,
        d.document_id,
        d.total_num, d.invoice_nr_norm, d.order_id_norm, d.merch_iban_ns, d.merch_bic_ns,
        d.merch_core_ns, d.custinfo_core_ns, d.currency_norm, d.document_unix
      FROM _docs_use d
      LEFT JOIN _cand c ON c.document_id = d.document_id
      WHERE c.document_id IS NULL
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id, NULL, rec.document_id,
        'considered','no_candidates',
        jsonb_build_object(
          'signals_present', jsonb_build_object(
            'amount',   rec.total_num IS NOT NULL,
            'invoice',  rec.invoice_nr_norm IS NOT NULL,
            'order',    rec.order_id_norm IS NOT NULL,
            'iban',     rec.merch_iban_ns IS NOT NULL,
            'bic',      rec.merch_bic_ns IS NOT NULL,
            'merchant', rec.merch_core_ns IS NOT NULL,
            'customer', rec.custinfo_core_ns IS NOT NULL,
            'currency', rec.currency_norm IS NOT NULL,
            'date',     rec.document_unix IS NOT NULL
          )
        )
      );
    END LOOP;
  END IF;

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- activity: below threshold, lost to winner, doc-level dedup excluded
  IF p_log_activity THEN
    FOR rec IN
      SELECT
        c.company_id_uuid, c.transaction_id, c.document_id,
        c.confidence, c.date_diff_days,
        c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
        c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match
      FROM _conf c
      WHERE c.confidence < p_min_confidence
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id_uuid, rec.transaction_id, rec.document_id,
        'skipped','below_min_confidence',
        jsonb_build_object(
          'confidence', rec.confidence,
          'weights', jsonb_build_object(
            'date', rec.date_weight, 'invoice', rec.invoice_weight, 'order', rec.order_weight,
            'merchant', rec.merchant_weight, 'amount', rec.amount_weight,
            'currency', rec.currency_weight, 'customer', rec.customer_weight
          ),
          'matches', jsonb_build_object(
            'date', rec.date_match, 'invoice', rec.invoice_match, 'order', rec.order_match,
            'merchant', rec.merchant_match, 'amount', rec.amount_match,
            'currency', rec.currency_match, 'customer', rec.customer_match
          )
        )
      );
    END LOOP;

    FOR rec IN
      WITH winners AS (
        SELECT company_id_txt, transaction_id, document_id FROM _best
      )
      SELECT
        c.company_id_uuid, c.transaction_id, c.document_id,
        c.confidence, c.date_diff_days
      FROM _conf c
      JOIN winners w
        ON w.company_id_txt = c.company_id_txt
       AND w.transaction_id = c.transaction_id
      WHERE c.confidence >= p_min_confidence
        AND c.document_id <> w.document_id
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id_uuid, rec.transaction_id, rec.document_id,
        'skipped','not_top_for_tx',
        jsonb_build_object('confidence', rec.confidence, 'date_diff_days', rec.date_diff_days)
      );
    END LOOP;

    FOR rec IN
      WITH kept AS (SELECT transaction_id, document_id FROM _best_use)
      SELECT
        b.company_id_uuid, b.transaction_id, b.document_id, b.confidence
      FROM _best b
      LEFT JOIN kept k
        ON k.transaction_id = b.transaction_id AND k.document_id = b.document_id
      WHERE k.transaction_id IS NULL
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id_uuid, rec.transaction_id, rec.document_id,
        'skipped','doc_dedup_excluded',
        jsonb_build_object('confidence', rec.confidence)
      );
    END LOOP;
  END IF;

  -- precompute already reconciled to log
  CREATE TEMP TABLE _already AS
  SELECT b.company_id_uuid, b.transaction_id, b.document_id
  FROM _best_use b
  JOIN public.reconciliations r
    ON r.company_id = b.company_id_uuid
   AND r.transaction_id = b.transaction_id
   AND r.document_id = b.document_id;

  IF p_log_activity THEN
    FOR rec IN
      SELECT * FROM _already
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id_uuid, rec.transaction_id, rec.document_id,
        'skipped','already_reconciled',
        jsonb_build_object('note','pair exists in public.reconciliations')
      );
    END LOOP;
  END IF;

  -- INSERT reconciliations
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'reconciliations_inserted'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_rows::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('per_row_ms', v_ms_each, 'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- write match scores
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  -- activity: final matches
  IF p_log_activity THEN
    FOR rec IN
      SELECT
        i.company_id, i.transaction_id, i.document_id,
        c.confidence, c.date_diff_days,
        c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match
      FROM _ins i
      JOIN _conf c
        ON c.company_id_uuid = i.company_id
       AND c.transaction_id  = i.transaction_id
       AND c.document_id     = i.document_id
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id, rec.transaction_id, rec.document_id,
        'matched', NULL,
        jsonb_build_object(
          'confidence', rec.confidence,
          'date_diff_days', rec.date_diff_days,
          'matches', jsonb_build_object(
            'date', rec.date_match, 'invoice', rec.invoice_match, 'order', rec.order_match,
            'merchant', rec.merchant_match, 'amount', rec.amount_match,
            'currency', rec.currency_match, 'customer', rec.customer_match
          )
        )
      );
    END LOOP;
  END IF;

  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'ok'::text,
    p_message           => NULL::text,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'error'::text,
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted_so_far', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v27(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v27'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false, p_log_activity boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
BEGIN
  -- optional: raise statement timeout for this run
  PERFORM set_config('statement_timeout','300000', true); -- 5 min

  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates,
                             'log_activity',     p_log_activity
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  -- gather stats for better plans
  ANALYZE _tx;
  ANALYZE _docs_use;
  ANALYZE _cand;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- activity logging (set-based, fast)
  IF p_log_activity THEN
    -- transactions with no candidates
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, tx.company_id_txt::uuid, tx.transaction_id, NULL,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   tx.debited_num IS NOT NULL,
          'invoice',  tx.invoice_nr_norm IS NOT NULL,
          'order',    tx.order_id_norm IS NOT NULL,
          'iban',     tx.tx_iban_ns IS NOT NULL,
          'bic',      tx.tx_bic_ns IS NOT NULL,
          'benef',    tx.benef_core_ns IS NOT NULL,
          'consumer', tx.consumer_core_ns IS NOT NULL,
          'currency', tx.currency_norm_tx IS NOT NULL,
          'date',     tx.completed_date IS NOT NULL
        )
      )
    FROM _tx tx
    LEFT JOIN _cand c ON c.transaction_id = tx.transaction_id
    WHERE c.transaction_id IS NULL;

    -- documents with no candidates
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, d.company_id_uuid, NULL, d.document_id,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   d.total_num IS NOT NULL,
          'invoice',  d.invoice_nr_norm IS NOT NULL,
          'order',    d.order_id_norm IS NOT NULL,
          'iban',     d.merch_iban_ns IS NOT NULL,
          'bic',      d.merch_bic_ns IS NOT NULL,
          'merchant', d.merch_core_ns IS NOT NULL,
          'customer', d.custinfo_core_ns IS NOT NULL,
          'currency', d.currency_norm IS NOT NULL,
          'date',     d.document_unix IS NOT NULL
        )
      )
    FROM _docs_use d
    LEFT JOIN _cand c ON c.document_id = d.document_id
    WHERE c.document_id IS NULL;

    -- below min confidence
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','below_min_confidence',
      jsonb_build_object(
        'confidence', c.confidence,
        'weights', jsonb_build_object(
          'date', c.date_weight, 'invoice', c.invoice_weight, 'order', c.order_weight,
          'merchant', c.merchant_weight, 'amount', c.amount_weight,
          'currency', c.currency_weight, 'customer', c.customer_weight
        ),
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _conf c
    WHERE c.confidence < p_min_confidence;

    -- not top for transaction
    WITH winners AS (
      SELECT company_id_txt, transaction_id, document_id FROM _best
    )
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','not_top_for_tx',
      jsonb_build_object('confidence', c.confidence, 'date_diff_days', c.date_diff_days)
    FROM _conf c
    JOIN winners w
      ON w.company_id_txt = c.company_id_txt
     AND w.transaction_id = c.transaction_id
    WHERE c.confidence >= p_min_confidence
      AND c.document_id <> w.document_id;

    -- dropped by doc-level dedup
    WITH kept AS (SELECT transaction_id, document_id FROM _best_use)
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, b.company_id_uuid, b.transaction_id, b.document_id,
      'skipped','doc_dedup_excluded',
      jsonb_build_object('confidence', b.confidence)
    FROM _best b
    LEFT JOIN kept k
      ON k.transaction_id = b.transaction_id AND k.document_id = b.document_id
    WHERE k.transaction_id IS NULL;

    -- already reconciled
    CREATE TEMP TABLE _already AS
    SELECT b.company_id_uuid, b.transaction_id, b.document_id
    FROM _best_use b
    JOIN public.reconciliations r
      ON r.company_id = b.company_id_uuid
     AND r.transaction_id = b.transaction_id
     AND r.document_id = b.document_id;

    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, a.company_id_uuid, a.transaction_id, a.document_id,
      'skipped','already_reconciled',
      jsonb_build_object('note','pair exists in public.reconciliations')
    FROM _already a;
  END IF;

  -- INSERT reconciliations
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'reconciliations_inserted'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_rows::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('per_row_ms', v_ms_each, 'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- match scores
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  -- activity: final matches
  IF p_log_activity THEN
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, i.company_id, i.transaction_id, i.document_id,
      'matched', NULL,
      jsonb_build_object(
        'confidence', c.confidence,
        'date_diff_days', c.date_diff_days,
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _ins i
    JOIN _conf c
      ON c.company_id_uuid = i.company_id
     AND c.transaction_id  = i.transaction_id
     AND c.document_id     = i.document_id;
  END IF;

  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'ok'::text,
    p_message           => NULL::text,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'error'::text,
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted_so_far', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v28(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v28'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false, p_log_activity boolean DEFAULT false, p_amount_tol_pct numeric DEFAULT 0.005, p_amount_tol_floor numeric DEFAULT 0.01, p_tol_requires_strong boolean DEFAULT true) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
BEGIN
  -- optional: raise statement timeout for this run
  PERFORM set_config('statement_timeout','300000', true); -- 5 min

  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates,
                             'log_activity',     p_log_activity,
                             'amount_tol_pct',   p_amount_tol_pct,
                             'amount_tol_floor', p_amount_tol_floor,
                             'tol_requires_strong', p_tol_requires_strong
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,

    NULLIF(regexp_replace(t.debited::text,  '[^\d\.-]', '', 'g'), '')::numeric  AS debited_num,
    NULLIF(regexp_replace(t.credited::text, '[^\d\.-]', '', 'g'), '')::numeric  AS credited_num,
    COALESCE(
      NULLIF(regexp_replace(t.debited::text,  '[^\d\.-]', '', 'g'), '')::numeric,
      NULLIF(regexp_replace(t.credited::text, '[^\d\.-]', '', 'g'), '')::numeric,
      NULLIF(regexp_replace(t.money_out::text,'[^\d\.-]', '', 'g'), '')::numeric,
      NULLIF(regexp_replace(t.money_in::text, '[^\d\.-]', '', 'g'), '')::numeric
    ) AS amount_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (amount_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs

  -- exact amount
  CREATE TEMP TABLE _cand_amount_exact AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.amount_num IS NOT NULL
   AND d.total_num = tx.amount_num;

  -- tolerant amount (percent), requires currency match and a strong cue unless disabled
  CREATE TEMP TABLE _cand_amount_tol AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.amount_num IS NOT NULL
   AND tx.currency_norm_tx = d.currency_norm
   AND abs(d.total_num - tx.amount_num) <= GREATEST(p_amount_tol_floor, d.total_num * p_amount_tol_pct)
   AND (
        NOT p_tol_requires_strong
        OR (
             (tx.invoice_nr_norm IS NOT NULL AND d.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = d.invoice_nr_norm)
          OR (tx.order_id_norm   IS NOT NULL AND d.order_id_norm   IS NOT NULL AND tx.order_id_norm   = d.order_id_norm)
          OR (tx.tx_iban_ns IS NOT NULL AND d.merch_iban_ns IS NOT NULL AND tx.tx_iban_ns = d.merch_iban_ns)
          OR (tx.tx_bic_ns  IS NOT NULL AND d.merch_bic_ns  IS NOT NULL AND tx.tx_bic_ns  = d.merch_bic_ns)
          OR (
               tx.benef_core_ns IS NOT NULL AND d.merch_core_ns IS NOT NULL
           AND length(tx.benef_core_ns) >= 3 AND length(d.merch_core_ns) >= 3
           AND (tx.benef_core_ns = d.merch_core_ns
                OR position(d.merch_core_ns in tx.benef_core_ns) > 0
                OR position(tx.benef_core_ns in d.merch_core_ns) > 0)
          )
          OR (
               tx.consumer_core_ns IS NOT NULL AND (
                 (d.custinfo_core_ns IS NOT NULL AND (
                     tx.consumer_core_ns = d.custinfo_core_ns
                     OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
                     OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
                 OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
               )
          )
        )
       );

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount_exact
    UNION ALL SELECT * FROM _cand_amount_tol
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  ANALYZE _tx;
  ANALYZE _docs_use;
  ANALYZE _cand;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.amount_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.amount_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    abs(c.total_num - c.amount_num) AS amount_delta,
    CASE WHEN c.total_num IS NULL OR c.amount_num IS NULL THEN NULL
         ELSE abs(c.total_num - c.amount_num) / NULLIF(c.total_num,0)
    END AS amount_delta_pct,
    (c.amount_num IS NOT NULL AND c.total_num IS NOT NULL
       AND abs(c.total_num - c.amount_num) <= GREATEST(p_amount_tol_floor, c.total_num * p_amount_tol_pct)) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match
          THEN rw.amount_weight *
               GREATEST(0, 1 - (
                 s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
               ))
          ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match
            THEN rw.amount_weight *
                 GREATEST(0, 1 - (
                   s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
                 ))
            ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match
                    THEN rw.amount_weight *
                         GREATEST(0, 1 - (
                           s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
                         ))
                    ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days,
    s.amount_delta
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text, c.amount_delta
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             amount_delta ASC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.amount_delta ASC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.amount_delta ASC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn, amount_delta
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- activity logging (set-based)
  IF p_log_activity THEN
    -- transactions with no candidates
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, tx.company_id_txt::uuid, tx.transaction_id, NULL,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   tx.amount_num IS NOT NULL,
          'invoice',  tx.invoice_nr_norm IS NOT NULL,
          'order',    tx.order_id_norm IS NOT NULL,
          'iban',     tx.tx_iban_ns IS NOT NULL,
          'bic',      tx.tx_bic_ns IS NOT NULL,
          'benef',    tx.benef_core_ns IS NOT NULL,
          'consumer', tx.consumer_core_ns IS NOT NULL,
          'currency', tx.currency_norm_tx IS NOT NULL,
          'date',     tx.completed_date IS NOT NULL
        )
      )
    FROM _tx tx
    LEFT JOIN _cand c ON c.transaction_id = tx.transaction_id
    WHERE c.transaction_id IS NULL;

    -- documents with no candidates
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, d.company_id_uuid, NULL, d.document_id,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   d.total_num IS NOT NULL,
          'invoice',  d.invoice_nr_norm IS NOT NULL,
          'order',    d.order_id_norm IS NOT NULL,
          'iban',     d.merch_iban_ns IS NOT NULL,
          'bic',      d.merch_bic_ns IS NOT NULL,
          'merchant', d.merch_core_ns IS NOT NULL,
          'customer', d.custinfo_core_ns IS NOT NULL,
          'currency', d.currency_norm IS NOT NULL,
          'date',     d.document_unix IS NOT NULL
        )
      )
    FROM _docs_use d
    LEFT JOIN _cand c ON c.document_id = d.document_id
    WHERE c.document_id IS NULL;

    -- below min confidence
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','below_min_confidence',
      jsonb_build_object(
        'confidence', c.confidence,
        'amount_delta', c.amount_delta,
        'weights', jsonb_build_object(
          'date', c.date_weight, 'invoice', c.invoice_weight, 'order', c.order_weight,
          'merchant', c.merchant_weight, 'amount', c.amount_weight,
          'currency', c.currency_weight, 'customer', c.customer_weight
        ),
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _conf c
    WHERE c.confidence < p_min_confidence;

    -- not top for transaction
    WITH winners AS (
      SELECT company_id_txt, transaction_id, document_id FROM _best
    )
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','not_top_for_tx',
      jsonb_build_object('confidence', c.confidence, 'date_diff_days', c.date_diff_days, 'amount_delta', c.amount_delta)
    FROM _conf c
    JOIN winners w
      ON w.company_id_txt = c.company_id_txt
     AND w.transaction_id = c.transaction_id
    WHERE c.confidence >= p_min_confidence
      AND c.document_id <> w.document_id;

    -- dropped by doc-level dedup
    WITH kept AS (SELECT transaction_id, document_id FROM _best_use)
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, b.company_id_uuid, b.transaction_id, b.document_id,
      'skipped','doc_dedup_excluded',
      jsonb_build_object('confidence', b.confidence, 'amount_delta', b.amount_delta)
    FROM _best b
    LEFT JOIN kept k
      ON k.transaction_id = b.transaction_id AND k.document_id = b.document_id
    WHERE k.transaction_id IS NULL;

    -- already reconciled
    CREATE TEMP TABLE _already AS
    SELECT b.company_id_uuid, b.transaction_id, b.document_id
    FROM _best_use b
    JOIN public.reconciliations r
      ON r.company_id = b.company_id_uuid
     AND r.transaction_id = b.transaction_id
     AND r.document_id = b.document_id;

    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, a.company_id_uuid, a.transaction_id, a.document_id,
      'skipped','already_reconciled',
      jsonb_build_object('note','pair exists in public.reconciliations')
    FROM _already a;
  END IF;

  -- INSERT reconciliations
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'reconciliations_inserted'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_rows::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('per_row_ms', v_ms_each, 'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- match scores
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  -- activity: final matches
  IF p_log_activity THEN
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, i.company_id, i.transaction_id, i.document_id,
      'matched', NULL,
      jsonb_build_object(
        'confidence', c.confidence,
        'date_diff_days', c.date_diff_days,
        'amount_delta', c.amount_delta,
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _ins i
    JOIN _conf c
      ON c.company_id_uuid = i.company_id
     AND c.transaction_id  = i.transaction_id
     AND c.document_id     = i.document_id;
  END IF;

  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'ok'::text,
    p_message           => NULL::text,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'error'::text,
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted_so_far', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v29(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v29'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false, p_log_activity boolean DEFAULT false, p_amount_tol_pct numeric DEFAULT 0.005, p_amount_tol_floor numeric DEFAULT 0.01, p_tol_requires_strong boolean DEFAULT true) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
BEGIN
  PERFORM set_config('statement_timeout','300000', true); -- 5 min

  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates,
                             'log_activity',     p_log_activity,
                             'amount_tol_pct',   p_amount_tol_pct,
                             'amount_tol_floor', p_amount_tol_floor,
                             'tol_requires_strong', p_tol_requires_strong
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions with direction
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,

    NULLIF(NULLIF(regexp_replace(t.credited::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) AS credited_num,
    NULLIF(NULLIF(regexp_replace(t.money_in::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) AS money_in_num,
    NULLIF(NULLIF(regexp_replace(t.debited::text,   '[^\d\.-]', '', 'g'), '')::numeric, 0) AS debited_num,
    NULLIF(NULLIF(regexp_replace(t.money_out::text, '[^\d\.-]', '', 'g'), '')::numeric, 0) AS money_out_num,

    COALESCE(
      NULLIF(NULLIF(regexp_replace(t.credited::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0),
      NULLIF(NULLIF(regexp_replace(t.money_in::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0),
      NULLIF(NULLIF(regexp_replace(t.debited::text,   '[^\d\.-]', '', 'g'), '')::numeric, 0),
      NULLIF(NULLIF(regexp_replace(t.money_out::text, '[^\d\.-]', '', 'g'), '')::numeric, 0)
    ) AS amount_num,

    (NULLIF(NULLIF(regexp_replace(t.credited::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL
     OR NULLIF(NULLIF(regexp_replace(t.money_in::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL)  AS tx_is_in,
    (NULLIF(NULLIF(regexp_replace(t.debited::text,   '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL
     OR NULLIF(NULLIF(regexp_replace(t.money_out::text, '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL) AS tx_is_out,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (amount_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs

  -- exact amount
  CREATE TEMP TABLE _cand_amount_exact AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.amount_num IS NOT NULL
   AND d.total_num = tx.amount_num;

  -- tolerant amount (percent), currency match + strong cue (direction-aware)
  CREATE TEMP TABLE _cand_amount_tol AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.amount_num IS NOT NULL
   AND tx.currency_norm_tx = d.currency_norm
   AND abs(d.total_num - tx.amount_num) <= GREATEST(p_amount_tol_floor, d.total_num * p_amount_tol_pct)
   AND (
        NOT p_tol_requires_strong
        OR (
             (tx.invoice_nr_norm IS NOT NULL AND d.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = d.invoice_nr_norm)
          OR (tx.order_id_norm   IS NOT NULL AND d.order_id_norm   IS NOT NULL AND tx.order_id_norm   = d.order_id_norm)
          OR (tx.tx_iban_ns IS NOT NULL AND d.merch_iban_ns IS NOT NULL AND tx.tx_iban_ns = d.merch_iban_ns)
          OR (tx.tx_bic_ns  IS NOT NULL AND d.merch_bic_ns  IS NOT NULL AND tx.tx_bic_ns  = d.merch_bic_ns)
          OR (tx.tx_is_out AND tx.benef_core_ns IS NOT NULL AND d.merch_core_ns IS NOT NULL AND
              (tx.benef_core_ns = d.merch_core_ns
               OR position(d.merch_core_ns in tx.benef_core_ns) > 0
               OR position(tx.benef_core_ns in d.merch_core_ns) > 0))
          OR (tx.tx_is_in AND tx.consumer_core_ns IS NOT NULL AND
              ( (d.custinfo_core_ns IS NOT NULL AND
                  (tx.consumer_core_ns = d.custinfo_core_ns
                   OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
                   OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
                OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)))
        )
       );

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount_exact
    UNION ALL SELECT * FROM _cand_amount_tol
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  ANALYZE _tx;
  ANALYZE _docs_use;
  ANALYZE _cand;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.amount_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.amount_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    abs(c.total_num - c.amount_num) AS amount_delta,
    CASE WHEN c.total_num IS NULL OR c.amount_num IS NULL THEN NULL
         ELSE abs(c.total_num - c.amount_num) / NULLIF(c.total_num,0)
    END AS amount_delta_pct,
    (c.amount_num IS NOT NULL AND c.total_num IS NOT NULL
       AND abs(c.total_num - c.amount_num) <= GREATEST(p_amount_tol_floor, c.total_num * p_amount_tol_pct)) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,

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

    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,

    (CASE WHEN s.amount_match
          THEN rw.amount_weight *
               GREATEST(0, 1 - (
                 s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
               ))
          ELSE 0 END) AS amount_score,

    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,

    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score_raw,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score_raw,

    (CASE WHEN s.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
          ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END)
     END) AS merchant_score,
    (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
          ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END)
     END) AS customer_score,

    ((s.tx_is_in AND s.customer_match) OR (s.tx_is_out AND s.merchant_match)) AS direction_ok,

    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.amount_match
            THEN rw.amount_weight *
                 GREATEST(0, 1 - (
                   s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
                 ))
            ELSE 0 END) +
      (CASE WHEN s.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
            ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END) END) +
      (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
            ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END) END)
    ) AS numerator,

    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.amount_match
                    THEN rw.amount_weight *
                         GREATEST(0, 1 - (
                           s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
                         ))
                    ELSE 0 END) +
              (CASE WHEN s.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
                    ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END) END) +
              (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
                    ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END) END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,

    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days,
    s.amount_delta
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text, c.amount_delta, c.direction_ok
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             c.direction_ok DESC,
             amount_delta ASC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.direction_ok DESC,
          b.amount_delta ASC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.direction_ok DESC,
                        b.amount_delta ASC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use (
       company_id_txt, company_id_uuid, transaction_id, document_id,
       confidence, numerator, total_weight, currency_weight, date_diff_days,
       fiscal_year, quarter, month_text, amount_delta, direction_ok, rn
    )
    SELECT
       company_id_txt, company_id_uuid, transaction_id, document_id,
       confidence, numerator, total_weight, currency_weight, date_diff_days,
       fiscal_year, quarter, month_text, amount_delta, direction_ok, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- activity logging (set-based)
  IF p_log_activity THEN
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, tx.company_id_txt::uuid, tx.transaction_id, NULL,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   tx.amount_num IS NOT NULL,
          'invoice',  tx.invoice_nr_norm IS NOT NULL,
          'order',    tx.order_id_norm IS NOT NULL,
          'iban',     tx.tx_iban_ns IS NOT NULL,
          'bic',      tx.tx_bic_ns IS NOT NULL,
          'benef',    tx.benef_core_ns IS NOT NULL,
          'consumer', tx.consumer_core_ns IS NOT NULL,
          'currency', tx.currency_norm_tx IS NOT NULL,
          'date',     tx.completed_date IS NOT NULL
        ),
        'direction', jsonb_build_object('in', tx.tx_is_in, 'out', tx.tx_is_out)
      )
    FROM _tx tx
    LEFT JOIN _cand c ON c.transaction_id = tx.transaction_id
    WHERE c.transaction_id IS NULL;

    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, d.company_id_uuid, NULL, d.document_id,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   d.total_num IS NOT NULL,
          'invoice',  d.invoice_nr_norm IS NOT NULL,
          'order',    d.order_id_norm IS NOT NULL,
          'iban',     d.merch_iban_ns IS NOT NULL,
          'bic',      d.merch_bic_ns IS NOT NULL,
          'merchant', d.merch_core_ns IS NOT NULL,
          'customer', d.custinfo_core_ns IS NOT NULL,
          'currency', d.currency_norm IS NOT NULL,
          'date',     d.document_unix IS NOT NULL
        )
      )
    FROM _docs_use d
    LEFT JOIN _cand c ON c.document_id = d.document_id
    WHERE c.document_id IS NULL;

    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','below_min_confidence',
      jsonb_build_object(
        'confidence', c.confidence,
        'amount_delta', c.amount_delta,
        'direction_ok', c.direction_ok,
        'weights', jsonb_build_object(
          'date', c.date_weight, 'invoice', c.invoice_weight, 'order', c.order_weight,
          'merchant', c.merchant_weight, 'amount', c.amount_weight,
          'currency', c.currency_weight, 'customer', c.customer_weight
        ),
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _conf c
    WHERE c.confidence < p_min_confidence;

    WITH winners AS (
      SELECT company_id_txt, transaction_id, document_id FROM _best
    )
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','not_top_for_tx',
      jsonb_build_object('confidence', c.confidence, 'date_diff_days', c.date_diff_days, 'amount_delta', c.amount_delta, 'direction_ok', c.direction_ok)
    FROM _conf c
    JOIN winners w
      ON w.company_id_txt = c.company_id_txt
     AND w.transaction_id = c.transaction_id
    WHERE c.confidence >= p_min_confidence
      AND c.document_id <> w.document_id;

    CREATE TEMP TABLE _already AS
    SELECT b.company_id_uuid, b.transaction_id, b.document_id
    FROM _best_use b
    JOIN public.reconciliations r
      ON r.company_id = b.company_id_uuid
     AND r.transaction_id = b.transaction_id
     AND r.document_id = b.document_id;

    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, a.company_id_uuid, a.transaction_id, a.document_id,
      'skipped','already_reconciled',
      jsonb_build_object('note','pair exists in public.reconciliations')
    FROM _already a;
  END IF;

  -- INSERT reconciliations
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'reconciliations_inserted'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_rows::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('per_row_ms', v_ms_each, 'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- match scores
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  -- activity: final matches
  IF p_log_activity THEN
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, i.company_id, i.transaction_id, i.document_id,
      'matched', NULL,
      jsonb_build_object(
        'confidence', c.confidence,
        'date_diff_days', c.date_diff_days,
        'amount_delta', c.amount_delta,
        'direction_ok', c.direction_ok,
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _ins i
    JOIN _conf c
      ON c.company_id_uuid = i.company_id
     AND c.transaction_id  = i.transaction_id
     AND c.document_id     = i.document_id;
  END IF;

  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'ok'::text,
    p_message           => NULL::text,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'error'::text,
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted_so_far', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v30(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v30'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false, p_log_activity boolean DEFAULT false, p_amount_tol_pct numeric DEFAULT 0.005, p_amount_tol_floor numeric DEFAULT 0.01, p_tol_requires_strong boolean DEFAULT true) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
BEGIN
  PERFORM set_config('statement_timeout','300000', true); -- 5 min

  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates,
                             'log_activity',     p_log_activity,
                             'amount_tol_pct',   p_amount_tol_pct,
                             'amount_tol_floor', p_amount_tol_floor,
                             'tol_requires_strong', p_tol_requires_strong
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions with direction
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,

    NULLIF(NULLIF(regexp_replace(t.credited::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) AS credited_num,
    NULLIF(NULLIF(regexp_replace(t.money_in::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) AS money_in_num,
    NULLIF(NULLIF(regexp_replace(t.debited::text,   '[^\d\.-]', '', 'g'), '')::numeric, 0) AS debited_num,
    NULLIF(NULLIF(regexp_replace(t.money_out::text, '[^\d\.-]', '', 'g'), '')::numeric, 0) AS money_out_num,

    COALESCE(
      NULLIF(NULLIF(regexp_replace(t.credited::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0),
      NULLIF(NULLIF(regexp_replace(t.money_in::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0),
      NULLIF(NULLIF(regexp_replace(t.debited::text,   '[^\d\.-]', '', 'g'), '')::numeric, 0),
      NULLIF(NULLIF(regexp_replace(t.money_out::text, '[^\d\.-]', '', 'g'), '')::numeric, 0)
    ) AS amount_num,

    (NULLIF(NULLIF(regexp_replace(t.credited::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL
     OR NULLIF(NULLIF(regexp_replace(t.money_in::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL)  AS tx_is_in,
    (NULLIF(NULLIF(regexp_replace(t.debited::text,   '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL
     OR NULLIF(NULLIF(regexp_replace(t.money_out::text, '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL) AS tx_is_out,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (amount_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs

  -- exact amount
  CREATE TEMP TABLE _cand_amount_exact AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.amount_num IS NOT NULL
   AND d.total_num = tx.amount_num;

  -- tolerant amount (percent), currency match + strong cue (direction-aware)
  CREATE TEMP TABLE _cand_amount_tol AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.amount_num IS NOT NULL
   AND tx.currency_norm_tx = d.currency_norm
   AND abs(d.total_num - tx.amount_num) <= GREATEST(p_amount_tol_floor, d.total_num * p_amount_tol_pct)
   AND (
        NOT p_tol_requires_strong
        OR (
             (tx.invoice_nr_norm IS NOT NULL AND d.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = d.invoice_nr_norm)
          OR (tx.order_id_norm   IS NOT NULL AND d.order_id_norm   IS NOT NULL AND tx.order_id_norm   = d.order_id_norm)
          OR (tx.tx_iban_ns IS NOT NULL AND d.merch_iban_ns IS NOT NULL AND tx.tx_iban_ns = d.merch_iban_ns)
          OR (tx.tx_bic_ns  IS NOT NULL AND d.merch_bic_ns  IS NOT NULL AND tx.tx_bic_ns  = d.merch_bic_ns)
          OR (tx.tx_is_out AND tx.benef_core_ns IS NOT NULL AND d.merch_core_ns IS NOT NULL AND
              (tx.benef_core_ns = d.merch_core_ns
               OR position(d.merch_core_ns in tx.benef_core_ns) > 0
               OR position(tx.benef_core_ns in d.merch_core_ns) > 0))
          OR (tx.tx_is_in AND tx.consumer_core_ns IS NOT NULL AND
              ( (d.custinfo_core_ns IS NOT NULL AND
                  (tx.consumer_core_ns = d.custinfo_core_ns
                   OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
                   OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
                OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)))
        )
       );

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount_exact
    UNION ALL SELECT * FROM _cand_amount_tol
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  ANALYZE _tx;
  ANALYZE _docs_use;
  ANALYZE _cand;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.amount_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.amount_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    abs(c.total_num - c.amount_num) AS amount_delta,
    CASE WHEN c.total_num IS NULL OR c.amount_num IS NULL THEN NULL
         ELSE abs(c.total_num - c.amount_num) / NULLIF(c.total_num,0)
    END AS amount_delta_pct,
    (c.amount_num IS NOT NULL AND c.total_num IS NOT NULL
       AND abs(c.total_num - c.amount_num) <= GREATEST(p_amount_tol_floor, c.total_num * p_amount_tol_pct)) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,

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

    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,

    (CASE WHEN s.amount_match
          THEN rw.amount_weight *
               GREATEST(0, 1 - (
                 s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
               ))
          ELSE 0 END) AS amount_score,

    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,

    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score_raw,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score_raw,

    (CASE WHEN s.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
          ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END)
     END) AS merchant_score,
    (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
          ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END)
     END) AS customer_score,

    ((s.tx_is_in AND s.customer_match) OR (s.tx_is_out AND s.merchant_match)) AS direction_ok,

    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.amount_match
            THEN rw.amount_weight *
                 GREATEST(0, 1 - (
                   s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
                 ))
            ELSE 0 END) +
      (CASE WHEN s.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
            ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END) END) +
      (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
            ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END) END)
    ) AS numerator,

    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.amount_match
                    THEN rw.amount_weight *
                         GREATEST(0, 1 - (
                           s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
                         ))
                    ELSE 0 END) +
              (CASE WHEN s.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
                    ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END) END) +
              (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
                    ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END) END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,

    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days,
    s.amount_delta
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text, c.amount_delta, c.direction_ok
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             c.direction_ok DESC,
             amount_delta ASC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.direction_ok DESC,
          b.amount_delta ASC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.direction_ok DESC,
                        b.amount_delta ASC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use (
       company_id_txt, company_id_uuid, transaction_id, document_id,
       confidence, numerator, total_weight, currency_weight, date_diff_days,
       fiscal_year, quarter, month_text, amount_delta, direction_ok, rn
    )
    SELECT
       company_id_txt, company_id_uuid, transaction_id, document_id,
       confidence, numerator, total_weight, currency_weight, date_diff_days,
       fiscal_year, quarter, month_text, amount_delta, direction_ok, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- activity logging (set-based)
  IF p_log_activity THEN
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, tx.company_id_txt::uuid, tx.transaction_id, NULL,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   tx.amount_num IS NOT NULL,
          'invoice',  tx.invoice_nr_norm IS NOT NULL,
          'order',    tx.order_id_norm IS NOT NULL,
          'iban',     tx.tx_iban_ns IS NOT NULL,
          'bic',      tx.tx_bic_ns IS NOT NULL,
          'benef',    tx.benef_core_ns IS NOT NULL,
          'consumer', tx.consumer_core_ns IS NOT NULL,
          'currency', tx.currency_norm_tx IS NOT NULL,
          'date',     tx.completed_date IS NOT NULL
        ),
        'direction', jsonb_build_object('in', tx.tx_is_in, 'out', tx.tx_is_out)
      )
    FROM _tx tx
    LEFT JOIN _cand c ON c.transaction_id = tx.transaction_id
    WHERE c.transaction_id IS NULL;

    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, d.company_id_uuid, NULL, d.document_id,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   d.total_num IS NOT NULL,
          'invoice',  d.invoice_nr_norm IS NOT NULL,
          'order',    d.order_id_norm IS NOT NULL,
          'iban',     d.merch_iban_ns IS NOT NULL,
          'bic',      d.merch_bic_ns IS NOT NULL,
          'merchant', d.merch_core_ns IS NOT NULL,
          'customer', d.custinfo_core_ns IS NOT NULL,
          'currency', d.currency_norm IS NOT NULL,
          'date',     d.document_unix IS NOT NULL
        )
      )
    FROM _docs_use d
    LEFT JOIN _cand c ON c.document_id = d.document_id
    WHERE c.document_id IS NULL;

    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','below_min_confidence',
      jsonb_build_object(
        'confidence', c.confidence,
        'amount_delta', c.amount_delta,
        'direction_ok', c.direction_ok,
        'weights', jsonb_build_object(
          'date', c.date_weight, 'invoice', c.invoice_weight, 'order', c.order_weight,
          'merchant', c.merchant_weight, 'amount', c.amount_weight,
          'currency', c.currency_weight, 'customer', c.customer_weight
        ),
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _conf c
    WHERE c.confidence < p_min_confidence;

    WITH winners AS (
      SELECT company_id_txt, transaction_id, document_id FROM _best
    )
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','not_top_for_tx',
      jsonb_build_object('confidence', c.confidence, 'date_diff_days', c.date_diff_days, 'amount_delta', c.amount_delta, 'direction_ok', c.direction_ok)
    FROM _conf c
    JOIN winners w
      ON w.company_id_txt = c.company_id_txt
     AND w.transaction_id = c.transaction_id
    WHERE c.confidence >= p_min_confidence
      AND c.document_id <> w.document_id;

    CREATE TEMP TABLE _already AS
    SELECT b.company_id_uuid, b.transaction_id, b.document_id
    FROM _best_use b
    JOIN public.reconciliations r
      ON r.company_id = b.company_id_uuid
     AND r.transaction_id = b.transaction_id
     AND r.document_id = b.document_id;

    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, a.company_id_uuid, a.transaction_id, a.document_id,
      'skipped','already_reconciled',
      jsonb_build_object('note','pair exists in public.reconciliations')
    FROM _already a;
  END IF;

  -- INSERT reconciliations
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'reconciliations_inserted'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_rows::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('per_row_ms', v_ms_each, 'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- write match scores (rounded to 2 decimals)
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    ROUND(c.confidence, 2),
    ROUND(c.numerator, 2),
    ROUND(c.total_weight, 2),
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    ROUND(c.date_score, 2), ROUND(c.invoice_score, 2), ROUND(c.order_score, 2),
    ROUND(c.merchant_score, 2), ROUND(c.amount_score, 2), ROUND(c.currency_score, 2), ROUND(c.customer_score, 2)
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  -- activity: final matches
  IF p_log_activity THEN
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, i.company_id, i.transaction_id, i.document_id,
      'matched', NULL,
      jsonb_build_object(
        'confidence', c.confidence,
        'date_diff_days', c.date_diff_days,
        'amount_delta', c.amount_delta,
        'direction_ok', c.direction_ok,
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _ins i
    JOIN _conf c
      ON c.company_id_uuid = i.company_id
     AND c.transaction_id  = i.transaction_id
     AND c.document_id     = i.document_id;
  END IF;

  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'ok'::text,
    p_message           => NULL::text,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'error'::text,
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted_so_far', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1_1(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v27'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false, p_log_activity boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
BEGIN
  -- optional: raise statement timeout for this run
  PERFORM set_config('statement_timeout','300000', true); -- 5 min
  RAISE NOTICE 'step: % ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)));
  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates,
                             'log_activity',     p_log_activity
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  -- gather stats for better plans
  ANALYZE _tx;
  ANALYZE _docs_use;
  ANALYZE _cand;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- activity logging (set-based, fast)
  IF p_log_activity THEN
    -- transactions with no candidates
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, tx.company_id_txt::uuid, tx.transaction_id, NULL,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   tx.debited_num IS NOT NULL,
          'invoice',  tx.invoice_nr_norm IS NOT NULL,
          'order',    tx.order_id_norm IS NOT NULL,
          'iban',     tx.tx_iban_ns IS NOT NULL,
          'bic',      tx.tx_bic_ns IS NOT NULL,
          'benef',    tx.benef_core_ns IS NOT NULL,
          'consumer', tx.consumer_core_ns IS NOT NULL,
          'currency', tx.currency_norm_tx IS NOT NULL,
          'date',     tx.completed_date IS NOT NULL
        )
      )
    FROM _tx tx
    LEFT JOIN _cand c ON c.transaction_id = tx.transaction_id
    WHERE c.transaction_id IS NULL;

    -- documents with no candidates
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, d.company_id_uuid, NULL, d.document_id,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   d.total_num IS NOT NULL,
          'invoice',  d.invoice_nr_norm IS NOT NULL,
          'order',    d.order_id_norm IS NOT NULL,
          'iban',     d.merch_iban_ns IS NOT NULL,
          'bic',      d.merch_bic_ns IS NOT NULL,
          'merchant', d.merch_core_ns IS NOT NULL,
          'customer', d.custinfo_core_ns IS NOT NULL,
          'currency', d.currency_norm IS NOT NULL,
          'date',     d.document_unix IS NOT NULL
        )
      )
    FROM _docs_use d
    LEFT JOIN _cand c ON c.document_id = d.document_id
    WHERE c.document_id IS NULL;

    -- below min confidence
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','below_min_confidence',
      jsonb_build_object(
        'confidence', c.confidence,
        'weights', jsonb_build_object(
          'date', c.date_weight, 'invoice', c.invoice_weight, 'order', c.order_weight,
          'merchant', c.merchant_weight, 'amount', c.amount_weight,
          'currency', c.currency_weight, 'customer', c.customer_weight
        ),
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _conf c
    WHERE c.confidence < p_min_confidence;

    -- not top for transaction
    WITH winners AS (
      SELECT company_id_txt, transaction_id, document_id FROM _best
    )
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','not_top_for_tx',
      jsonb_build_object('confidence', c.confidence, 'date_diff_days', c.date_diff_days)
    FROM _conf c
    JOIN winners w
      ON w.company_id_txt = c.company_id_txt
     AND w.transaction_id = c.transaction_id
    WHERE c.confidence >= p_min_confidence
      AND c.document_id <> w.document_id;

    -- dropped by doc-level dedup
    WITH kept AS (SELECT transaction_id, document_id FROM _best_use)
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, b.company_id_uuid, b.transaction_id, b.document_id,
      'skipped','doc_dedup_excluded',
      jsonb_build_object('confidence', b.confidence)
    FROM _best b
    LEFT JOIN kept k
      ON k.transaction_id = b.transaction_id AND k.document_id = b.document_id
    WHERE k.transaction_id IS NULL;

    -- already reconciled
    CREATE TEMP TABLE _already AS
    SELECT b.company_id_uuid, b.transaction_id, b.document_id
    FROM _best_use b
    JOIN public.reconciliations r
      ON r.company_id = b.company_id_uuid
     AND r.transaction_id = b.transaction_id
     AND r.document_id = b.document_id;

    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, a.company_id_uuid, a.transaction_id, a.document_id,
      'skipped','already_reconciled',
      jsonb_build_object('note','pair exists in public.reconciliations')
    FROM _already a;
  END IF;

  -- INSERT reconciliations
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'reconciliations_inserted'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_rows::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('per_row_ms', v_ms_each, 'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- match scores
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_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
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  -- activity: final matches
  IF p_log_activity THEN
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, i.company_id, i.transaction_id, i.document_id,
      'matched', NULL,
      jsonb_build_object(
        'confidence', c.confidence,
        'date_diff_days', c.date_diff_days,
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _ins i
    JOIN _conf c
      ON c.company_id_uuid = i.company_id
     AND c.transaction_id  = i.transaction_id
     AND c.document_id     = i.document_id;
  END IF;

  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'ok'::text,
    p_message           => NULL::text,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'error'::text,
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted_so_far', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

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

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

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

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

create or replace function get_schema_json()
    returns TABLE(schema jsonb)
    stable
    language sql
as
$$
  select jsonb_agg(
    jsonb_build_object(
      'table_schema', n.nspname,
      'table_name', c.relname,
      'columns', (
        select jsonb_agg(
          jsonb_build_object(
            'column_name', a.attname,
            'data_type', format_type(a.atttypid, a.atttypmod),
            'default', pg_get_expr(d.adbin, d.adrelid)
          ) order by a.attnum
        )
        from pg_attribute a
        join pg_type t on a.atttypid = t.oid
        left join pg_attrdef d on a.attrelid = d.adrelid and a.attnum = d.adnum
        where a.attrelid = c.oid
          and a.attnum > 0
          and not a.attisdropped
      )
    )
  ) as schema
  from pg_class c
  join pg_namespace n on n.oid = c.relnamespace
  where c.relkind = 'r'
    and n.nspname not in ('pg_catalog', 'information_schema');
$$;

alter function get_schema_json() owner to postgres;

grant execute on function get_schema_json() to anon;

grant execute on function get_schema_json() to authenticated;

grant execute on function get_schema_json() to service_role;

create or replace function athena_reconcile_general_v1_1(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  status_text text := 'ok';
  message_text text := 'success';

  -- engine/versioning
  engine_version text := '1.1';

  -- timing
  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean;
  config_log_activity boolean;

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;

  -- logging batch
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time numeric := EXTRACT(EPOCH FROM now())::bigint;
BEGIN
  /* 1) load configuration */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

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

  /* 1a) derive configuration */
  derived_amount_tolerance_fraction := CASE WHEN config_amount_tolerance >= 1
                              THEN config_amount_tolerance / 100.0
                              ELSE config_amount_tolerance END;
  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

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

  /* 2) resolve weights */
  weights_json :=
    COALESCE((
      WITH candidate_weights AS (
        SELECT w.*
        FROM public.reconciliations_weight_registry w
        WHERE w.active
          AND (
               (p_company_id IS NULL AND w.company_id IS NULL AND w.merchant_id IS NULL)
            OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR (w.company_id IS NULL AND w.merchant_id IS NULL)))
          )
      ),
      picked_weights AS (
        SELECT DISTINCT ON (weight_key) weight_key, weight_value, company_id
        FROM candidate_weights
        ORDER BY weight_key, CASE WHEN company_id IS NULL THEN 1 ELSE 0 END
      )
      SELECT jsonb_object_agg(weight_key, weight_value) FROM picked_weights
    ), '{}'::jsonb);

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

  /* 3) pipeline + logging + inserts */
  WITH
  document_scope AS (
    SELECT document_id, company_id, total, currency, invoice_number, order_id,
           document_date_unix, merchant_id, customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND COALESCE(d.reconciled,false) = false
      AND NOT EXISTS (SELECT 1 FROM public.reconciliations r WHERE r.document_id = d.document_id)
  ),
  transactions_scope AS (
    SELECT transaction_id, company_id, amount, debited, credited, currency, invoice_nr, order_id,
           completed_date, merchant_id, name_consumer
    FROM public.transactions t
    WHERE (p_company_id IS NULL OR t.company_id = p_company_id::text)
      AND COALESCE(t.reconciled,false) = false
      AND NOT EXISTS (SELECT 1 FROM public.reconciliations r WHERE r.transaction_id = t.transaction_id)
      AND (
           invoice_nr IS NOT NULL
        OR order_id   IS NOT NULL
        OR (currency IS NOT NULL AND merchant_id   IS NOT NULL)
        OR (currency IS NOT NULL AND name_consumer IS NOT NULL)
        OR (currency IS NOT NULL AND (amount IS NOT NULL OR debited IS NOT NULL OR credited IS NOT NULL))
      )
    ORDER BY completed_date DESC, transaction_id
    LIMIT derived_max_transactions
  ),

  candidate_pairs AS (
    SELECT t.transaction_id AS transaction_id, d.document_id AS 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 IS NOT NULL AND d.invoice_number <> '' AND t.invoice_nr IS NOT NULL AND t.invoice_nr <> '' AND d.invoice_number = t.invoice_nr)
          OR
          (d.order_id IS NOT NULL AND d.order_id <> '' AND t.order_id IS NOT NULL AND t.order_id <> '' AND d.order_id = t.order_id)
          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
          (d.currency = t.currency AND (
             d.total = t.debited OR d.total = t.credited OR d.total = ABS(t.amount) OR d.total = t.amount
          ))
          OR
          (d.currency = t.currency AND d.total IS NOT NULL
            AND (
              CASE
                WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN ABS(d.total - t.debited)
                WHEN t.credited IS NOT NULL AND t.credited > 0 THEN ABS(d.total - t.credited)
                WHEN t.amount   IS NOT NULL                  THEN ABS(d.total - ABS(t.amount))
                ELSE NULL
              END
            ) IS NOT NULL
            AND (
              CASE
                WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN ABS(d.total - t.debited)
                WHEN t.credited IS NOT NULL AND t.credited > 0 THEN ABS(d.total - t.credited)
                WHEN t.amount   IS NOT NULL                  THEN ABS(d.total - ABS(t.amount))
                ELSE 999999999
              END
            ) <= d.total * derived_amount_tolerance_fraction
          )
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND d.invoice_number <> '' AND t.invoice_nr IS NOT NULL AND t.invoice_nr <> '' AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id IS NOT NULL AND d.order_id <> '' AND t.order_id IS NOT NULL AND t.order_id <> '' AND d.order_id = t.order_id THEN 1
          WHEN d.currency = t.currency AND (
             d.total = t.debited OR d.total = t.credited OR d.total = ABS(t.amount) OR d.total = t.amount
          ) THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 6)
    ) d ON TRUE
  ),
  distinct_pairs AS (
    SELECT DISTINCT cp.transaction_id, cp.document_id FROM candidate_pairs cp
  ),

  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 AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      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.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name
    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,

      CASE
        WHEN a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 THEN a.transaction_debited
        WHEN a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 THEN a.transaction_credited
        WHEN a.transaction_amount   IS NOT NULL                              THEN ABS(a.transaction_amount)
        ELSE NULL
      END AS transaction_effective_amount,

      COALESCE(
        (a.transaction_invoice_number IS NOT NULL AND a.transaction_invoice_number <> ''
         AND a.document_invoice_number IS NOT NULL AND a.document_invoice_number <> ''
         AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,

      COALESCE(
        (a.transaction_order_id IS NOT NULL AND a.transaction_order_id <> ''
         AND a.document_order_id IS NOT NULL AND a.document_order_id <> ''
         AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,

      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,

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

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

      (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 * derived_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 * derived_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.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 * derived_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((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),

  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total) / NULLIF(m.document_total * derived_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.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::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.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 weights_max_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)
                        / weights_max_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
  ),

  -- thresholds
  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 < config_min_confidence
  ),
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= config_min_confidence
  ),

  -- deterministic top-1 per transaction
  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
  ),

  -- deterministic top-1 per document
  ranked_per_document AS (
    SELECT
      t.*,
      ROW_NUMBER() OVER (
        PARTITION BY t.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_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
  ),

  -- block if transaction OR document already present in reconciliations (any past run)
  blocked_existing AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       OR EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.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,            -- unix time (numeric)
      engine_version,  -- engine version tag
      fiscal_year,     -- numeric
      quarter,         -- numeric 1-4
      month,           -- text month name
      summary          -- TEXT to preserve key order
    )
    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, log_run_unix_time)::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT
        -- from final_candidates d (pick only what you need)
        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,

        -- derived timestamp base
        t.base_unix,

        -- from attributes a (tx/doc raw fields for summary)
        a.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      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,
      e.confidence_percent,
      log_run_unix_time,
      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.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_max_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 */
  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 (
    SELECT
      i.reconciliation_id,
      log_run_id        AS run_id,
      log_run_unix_time AS time_unix,
      lb.company_id, i.transaction_id, i.document_id,
      'matched'::text AS action,
      NULL::text      AS reason,
      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
      ) AS details
    FROM inserted_reconciliations i
    JOIN logging_base lb ON lb.transaction_id = i.transaction_id AND lb.document_id = i.document_id
  ),

  logging_skipped_union AS (
    SELECT log_run_id AS run_id, log_run_unix_time AS time_unix, lb.company_id, x.transaction_id, x.document_id,
           'skipped'::text AS action, 'not_top_for_tx'::text AS reason,
           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
           ) AS details
    FROM not_top_for_transaction x JOIN logging_base lb USING (transaction_id, document_id)
    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
           ) AS details
    FROM not_top_for_document x JOIN logging_base lb USING (transaction_id, document_id)
    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
           ) AS details
    FROM below_min_confidence x JOIN logging_base lb USING (transaction_id, document_id)
    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.amount_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
           ) AS details
    FROM low_signal x JOIN logging_base lb USING (transaction_id, document_id)
    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','blocked_existing_tx_or_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
           ) AS details
    FROM blocked_existing x JOIN logging_base lb USING (transaction_id, document_id)
  ),

  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 top_per_transaction_count, top_per_document_count, passed_confidence_count
  INTO   summary_top_per_transaction_count, summary_top_per_document_count, summary_passed_confidence_count
  FROM final_counts;

  -- timing
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  RETURN jsonb_build_object(
    'msg',               message_text,
    '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
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1_2(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  message_text text := 'success';
  engine_version text := '1.2';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean; -- reserved
  config_log_activity boolean;

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;

  -- run meta
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time numeric := EXTRACT(EPOCH FROM now())::bigint;

  -- advisory lock
  lock_acquired boolean := true;
BEGIN
  /* 0) single-company run guard */
  lock_acquired := pg_try_advisory_xact_lock(
                     hashtext('athena_reconcile_general_v1_2'),
                     COALESCE(hashtext(p_company_id::text), 0)
                   );
  IF NOT lock_acquired THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;
    RETURN jsonb_build_object('msg','another run in progress','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* 1) load configuration */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

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

  /* 1a) derive configuration */
  derived_amount_tolerance_fraction :=
    CASE WHEN config_amount_tolerance >= 1
         THEN config_amount_tolerance / 100.0
         ELSE config_amount_tolerance
    END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

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

  /* 2) resolve weights */
  weights_json :=
    COALESCE((
      WITH candidate_weights AS (
        SELECT w.*
        FROM public.reconciliations_weight_registry w
        WHERE w.active
          AND (
               (p_company_id IS NULL AND w.company_id IS NULL AND w.merchant_id IS NULL)
            OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR (w.company_id IS NULL AND w.merchant_id IS NULL)))
          )
      ),
      picked_weights AS (
        SELECT DISTINCT ON (weight_key) weight_key, weight_value, company_id
        FROM candidate_weights
        ORDER BY weight_key, CASE WHEN company_id IS NULL THEN 1 ELSE 0 END
      )
      SELECT jsonb_object_agg(weight_key, weight_value) FROM picked_weights
    ), '{}'::jsonb);

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

  /* 3) pipeline + inline logging */
  WITH
  document_scope AS (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),

  transactions_scope AS (
    SELECT
      t.transaction_id,
      t.company_id,                              -- text
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      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.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 derived_max_transactions
  ),

  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 IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR
          (d.order_id   IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id   = t.order_id)
          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
          (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 * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 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 AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      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.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name,
      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.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
         AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,

      COALESCE(
        (a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
         AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,

      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,

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

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

      (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 * derived_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 * derived_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.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 * derived_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((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),

  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total) / NULLIF(m.document_total * derived_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.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::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.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 weights_max_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)
                        / weights_max_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
  ),

  /* thresholds and ranking */
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= 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
  ),
  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.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  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
  ),

  /* single membership gate */
  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, 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.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      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,
      e.confidence_percent,
      log_run_unix_time,
      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.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_max_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
  ),

  /* base for logs */
  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
  ),

  /* losers for logging */
  not_top_for_transaction AS (
    SELECT * FROM (
      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 rn
      FROM base_candidates b
    ) q WHERE rn > 1
  ),
  not_top_for_document AS (
    SELECT * FROM (
      SELECT
        t.*,
        ROW_NUMBER() OVER (
          PARTITION BY t.document_id
          ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
        ) AS rn
      FROM top_per_transaction t
    ) q WHERE rn > 1
  ),

  /* matched -> reconciliation_activity */
  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        AS run_id,
      log_run_unix_time AS time,
      lb.company_id, i.transaction_id, i.document_id,
      'matched'::text   AS action,
      NULL::text        AS reason,
      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
  ),

  /* skipped -> reconciliations_activity */
  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 (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_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,  -- correct
               '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 (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  /* final counts for return */
  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 top_per_transaction_count, top_per_document_count, passed_confidence_count
  INTO   summary_top_per_transaction_count, summary_top_per_document_count, summary_passed_confidence_count
  FROM final_counts;

  -- timing
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  RETURN jsonb_build_object(
    'msg',               message_text,
    '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
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1_3(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  message_text text := 'success';
  engine_version text := '1.3';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean; -- reserved
  config_log_activity boolean;

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;
  v_inserted_count bigint := 0;
  v_candidates_count bigint := 0;

  -- run meta
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time numeric := EXTRACT(EPOCH FROM now())::bigint;

  -- advisory lock
  lock_acquired boolean := true;

  -- error diagnostics
  err_state   text;
  err_msg     text;
  err_detail  text;
  err_hint    text;
  err_context text;
BEGIN
  /* single-company run guard */
  lock_acquired := pg_try_advisory_xact_lock(
                     hashtext('athena_reconcile_general_v1_3'),
                     COALESCE(hashtext(p_company_id::text), 0)
                   );
  IF NOT lock_acquired THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;
    RETURN jsonb_build_object('msg','another run in progress','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* load configuration */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

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

  /* derive configuration */
  derived_amount_tolerance_fraction :=
    CASE WHEN config_amount_tolerance >= 1
         THEN config_amount_tolerance / 100.0
         ELSE config_amount_tolerance
    END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

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

  /* resolve weights (normalized, company-first, safe fallback) */
  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_weights AS (
    SELECT DISTINCT ON (k_norm)
           k_norm, v_norm
    FROM candidate_weights
    ORDER BY k_norm, (company_id IS NULL) -- FALSE(company) before TRUE(global)
  ),
  wj AS (
    SELECT jsonb_object_agg(k_norm, v_norm) AS j
    FROM picked_weights
  )
  SELECT COALESCE(j, '{}'::jsonb) INTO weights_json FROM wj;

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

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

  /* run_start log including resolved weights */
  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_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', config_log_activity,
        'allow_doc_dups', config_allow_document_duplicates,
        'amount_tol_pct', derived_amount_tolerance_fraction,
        'min_confidence', config_min_confidence,
        'date_window_days', config_date_window_days,
        'max_tx_seed', derived_max_transactions,
        'seed_cap_per_criterion', derived_seed_cap_per_criterion
      ),
      'weights_json', weights_json,
      'weights_max_possible', weights_max_possible
    )
  );

  /* ---------- main pipeline ---------- */
  WITH
  document_scope AS (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),
  transactions_scope AS (
    SELECT
      t.transaction_id,
      t.company_id,      -- text
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      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.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 derived_max_transactions
  ),
  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 IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR (d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id)
          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 (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 * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 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 AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      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.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name,
      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.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
                AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,
      COALESCE((a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
                AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,
      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,
      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= derived_date_window_seconds, FALSE) AS date_match,
      a.document_total, a.document_company_id, a.transaction_date_unix, a.document_date_unix,
      (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 * derived_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 * derived_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.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 * derived_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((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),
  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,
      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total)
                                        / NULLIF(m.document_total * derived_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.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,
      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::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.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 weights_max_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)
                        / weights_max_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
  ),
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= 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
  ),
  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.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  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, 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.transaction_invoice_number, a.document_invoice_number,
        a.transaction_order_id, a.document_order_id,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name
      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,
      log_run_unix_time,
      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.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_max_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
  ),
  not_top_for_transaction AS (
    SELECT * FROM (
      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 rn
      FROM base_candidates b
    ) q WHERE rn > 1
  ),
  not_top_for_document AS (
    SELECT * FROM (
      SELECT t.*, ROW_NUMBER() OVER (
        PARTITION BY t.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rn
      FROM top_per_transaction t
    ) q WHERE rn > 1
  ),
  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 (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_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 (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) 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;

  /* timing */
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  /* run_end 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 (
    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',               message_text,
    '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_v1_3(unknown, unknown) owner to postgres;

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

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

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

create or replace function athena_reconcile_general_v1_5(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  message_text text := 'success';
  engine_version text := '1.5';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean; -- reserved
  config_log_activity boolean;

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;

  -- run meta
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time bigint := EXTRACT(EPOCH FROM now())::bigint;

  -- for reconciliations_logs
  v_candidates_count bigint := 0;
  v_inserted_count bigint := 0;

  -- advisory lock
  lock_acquired boolean := true;
BEGIN
  -- 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 (
    log_run_id, engine_version, 'run_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'reconciliation_config_id', p_reconciliation_config_id,
        'max_transactions', derived_max_transactions,
        'seed_cap_per_criterion', derived_seed_cap_per_criterion
      )
    )
  );

  /* 0) single-company run guard */
  lock_acquired := pg_try_advisory_xact_lock(
                     hashtext('athena_reconcile_general_v1_3'),
                     COALESCE(hashtext(COALESCE(p_company_id::text,'__all__')), 0)
                   );
  IF NOT lock_acquired THEN
    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', 'another run in progress',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms,
      jsonb_build_object('note','lock not acquired')
    );

    RETURN jsonb_build_object(
      'msg','another run in progress','time_ms',elapsed_ms,
      'top_per_tx',0,'top_per_doc',0,'passed_confidence',0
    );
  END IF;

  /* 1) load configuration */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    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', 'error', 'reconciliations_config not found',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms, NULL
    );

    RETURN jsonb_build_object('msg','reconciliations_config not found','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* 1a) derive configuration */
  derived_amount_tolerance_fraction :=
    CASE WHEN config_amount_tolerance >= 1
         THEN config_amount_tolerance / 100.0
         ELSE config_amount_tolerance
    END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

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

  /* 2) resolve weights */
  weights_json :=
    COALESCE((
      WITH candidate_weights AS (
        SELECT w.*
        FROM public.reconciliations_weight_registry w
        WHERE w.active
          AND (
               (p_company_id IS NULL AND w.company_id IS NULL AND w.merchant_id IS NULL)
            OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR (w.company_id IS NULL AND w.merchant_id IS NULL)))
          )
      ),
      picked_weights AS (
        SELECT DISTINCT ON (weight_key) weight_key, weight_value, company_id
        FROM candidate_weights
        ORDER BY weight_key, CASE WHEN company_id IS NULL THEN 1 ELSE 0 END
      )
      SELECT jsonb_object_agg(weight_key, weight_value) FROM picked_weights
    ), '{}'::jsonb);

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

  /* 3) pipeline + inserts + activity logs + pair cache updates */
  WITH
  document_scope AS (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),

  transactions_scope AS (
    SELECT
      t.transaction_id,
      t.company_id,                              -- text
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      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.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 derived_max_transactions
  ),

  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 IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR
          (d.order_id   IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id   = t.order_id)
          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
          (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 * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 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 AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      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.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name,
      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.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
         AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,

      COALESCE(
        (a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
         AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,

      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,

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

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

      (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 * derived_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 * derived_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.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 * derived_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((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),

  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total) / NULLIF(m.document_total * derived_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.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::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.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 weights_max_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)
                        / weights_max_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
  ),

  /* thresholds and ranking */
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= 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
  ),
  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.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  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
  ),

  /* single membership gate */
  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, 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.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      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,
      e.confidence_percent,
      log_run_unix_time,
      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.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_max_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
  ),

  /* base for activity logs */
  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
  ),

  /* losers for logging and cache */
  not_top_for_transaction AS (
    SELECT * FROM (
      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 rn
      FROM base_candidates b
    ) q WHERE rn > 1
  ),
  not_top_for_document AS (
    SELECT * FROM (
      SELECT
        t.*,
        ROW_NUMBER() OVER (
          PARTITION BY t.document_id
          ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
        ) AS rn
      FROM top_per_transaction t
    ) q WHERE rn > 1
  ),

  /* matched -> reconciliation_activity (if enabled) */
  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        AS run_id,
      log_run_unix_time AS time,
      lb.company_id, i.transaction_id, i.document_id,
      'matched'::text   AS action,
      NULL::text        AS reason,
      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
  ),

  /* skipped -> reconciliations_activity (if enabled) */
  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 (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_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 (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  /* pair cache: matched */
  cache_upsert_matched AS (
    INSERT INTO public.reconciliations_pair_cache_id (
      company_id, transaction_id, document_id, last_action, last_confidence, last_seen_unix
    )
    SELECT
      lb.company_id, i.transaction_id, i.document_id,
      'matched', lb.confidence_percent, log_run_unix_time
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    ON CONFLICT (transaction_id, document_id) DO UPDATE
    SET last_action     = EXCLUDED.last_action,
        last_confidence = EXCLUDED.last_confidence,
        last_seen_unix  = EXCLUDED.last_seen_unix
    RETURNING 1
  ),

  /* pair cache: skipped */
  cache_upsert_skipped AS (
    INSERT INTO public.reconciliations_pair_cache_id (
      company_id, transaction_id, document_id, last_action, last_confidence, last_seen_unix
    )
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM not_top_for_transaction x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM not_top_for_document x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_min_confidence
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    ON CONFLICT (transaction_id, document_id) DO UPDATE
    SET last_action     = EXCLUDED.last_action,
        last_confidence = EXCLUDED.last_confidence,
        last_seen_unix  = EXCLUDED.last_seen_unix
    RETURNING 1
  ),

  /* final counts for return and logging */
  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM top_per_transaction)  AS c_top_per_transaction,
      (SELECT COUNT(*) FROM top_per_document)     AS c_top_per_document,
      (SELECT COUNT(*) FROM final_candidates)     AS c_passed_confidence,
      (SELECT COUNT(*) FROM base_candidates)      AS c_base_candidates,
      (SELECT COUNT(*) FROM inserted_reconciliations) AS c_inserted
  )
  SELECT
    c_top_per_transaction, c_top_per_document, c_passed_confidence,
    c_base_candidates, c_inserted
  INTO
    summary_top_per_transaction_count, summary_top_per_document_count, summary_passed_confidence_count,
    v_candidates_count, v_inserted_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',               message_text,
    '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;

  BEGIN
    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', SQLERRM,
      p_company_id, NULL, NULL, NULL,
      NULL, NULL, NULL, elapsed_ms,
      jsonb_build_object('hint','see server logs or activity tables')
    );
  EXCEPTION WHEN OTHERS THEN
    -- swallow logging failure
    NULL;
  END;

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

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

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

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

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

create or replace function athena_reconcile_general_v1_6(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  message_text text := 'success';
  engine_version text := '1.6';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean; -- reserved
  config_log_activity boolean;
  config_use_pair_cache boolean := false;   -- NEW

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;

  -- run meta
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time bigint := EXTRACT(EPOCH FROM now())::bigint;

  -- for reconciliations_logs
  v_candidates_count bigint := 0;
  v_inserted_count bigint := 0;

  -- advisory lock
  lock_acquired boolean := true;
BEGIN
  /* 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 (
    log_run_id, engine_version, 'run_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'reconciliation_config_id', p_reconciliation_config_id,
        'max_transactions', derived_max_transactions,
        'seed_cap_per_criterion', derived_seed_cap_per_criterion
      )
    )
  );

  /* 0) single-company run guard */
  lock_acquired := pg_try_advisory_xact_lock(
                     hashtext('athena_reconcile_general_v1_6'),
                     COALESCE(hashtext(COALESCE(p_company_id::text,'__all__')), 0)
                   );
  IF NOT lock_acquired THEN
    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', 'another run in progress',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms,
      jsonb_build_object('note','lock not acquired')
    );

    RETURN jsonb_build_object(
      'msg','another run in progress','time_ms',elapsed_ms,
      'top_per_tx',0,'top_per_doc',0,'passed_confidence',0
    );
  END IF;

  /* 1) load configuration (includes use_pair_cache) */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity, COALESCE(use_pair_cache,false)
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity, config_use_pair_cache
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    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', 'error', 'reconciliations_config not found',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms, NULL
    );

    RETURN jsonb_build_object('msg','reconciliations_config not found','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* 1a) derive configuration */
  derived_amount_tolerance_fraction :=
    CASE WHEN config_amount_tolerance >= 1
         THEN config_amount_tolerance / 100.0
         ELSE config_amount_tolerance
    END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

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

  /* 2) resolve weights */
  weights_json :=
    COALESCE((
      WITH candidate_weights AS (
        SELECT w.*
        FROM public.reconciliations_weight_registry w
        WHERE w.active
          AND (
               (p_company_id IS NULL AND w.company_id IS NULL AND w.merchant_id IS NULL)
            OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR (w.company_id IS NULL AND w.merchant_id IS NULL)))
          )
      ),
      picked_weights AS (
        SELECT DISTINCT ON (weight_key) weight_key, weight_value, company_id
        FROM candidate_weights
        ORDER BY weight_key, CASE WHEN company_id IS NULL THEN 1 ELSE 0 END
      )
      SELECT jsonb_object_agg(weight_key, weight_value) FROM picked_weights
    ), '{}'::jsonb);

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

  /* 3) pipeline + inserts + activity logs + pair cache updates */
  WITH
  document_scope AS (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),

  transactions_scope AS (
    SELECT
      t.transaction_id,
      t.company_id,                              -- text
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      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.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 derived_max_transactions
  ),

  /* cache: known-good matched pairs, enabled only if config_use_pair_cache */
  cache_pairs AS (
    SELECT c.transaction_id, c.document_id
    FROM public.reconciliations_pair_cache_id c
    WHERE config_use_pair_cache
      AND (p_company_id IS NULL OR c.company_id = p_company_id)
      AND c.last_action = 'matched'
  ),

  seeded_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN cache_pairs c ON c.transaction_id = t.transaction_id
    JOIN document_scope d ON d.document_id = c.document_id
    WHERE NOT EXISTS (SELECT 1 FROM public.reconciliations r WHERE r.transaction_id = t.transaction_id)
      AND NOT EXISTS (SELECT 1 FROM public.reconciliations r WHERE r.document_id    = d.document_id)
  ),

  candidate_pairs AS (
    SELECT * FROM seeded_pairs
    UNION ALL
    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 (
          -- if cache disabled, this clause is neutral;
          -- if enabled, it drops known-bad pairs
          (NOT config_use_pair_cache) OR NOT EXISTS (
            SELECT 1
            FROM public.reconciliations_pair_cache_id c
            WHERE c.transaction_id = t.transaction_id
              AND c.document_id    = d.document_id
              AND c.last_action    = 'skipped'
          )
        )
        AND (
          (d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR
          (d.order_id   IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id   = t.order_id)
          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
          (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 * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 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 AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      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.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name,
      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.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
         AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,

      COALESCE(
        (a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
         AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,

      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,

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

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

      (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 * derived_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 * derived_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.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 * derived_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((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),

  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total) / NULLIF(m.document_total * derived_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.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::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.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 weights_max_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)
                        / weights_max_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
  ),

  /* thresholds and ranking */
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= 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
  ),
  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.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  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
  ),

  /* single membership gate */
  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, 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.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      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,
      e.confidence_percent,
      log_run_unix_time,
      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.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_max_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
  ),

  /* base for activity logs */
  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
  ),

  /* losers for logging and cache */
  not_top_for_transaction AS (
    SELECT * FROM (
      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 rn
      FROM base_candidates b
    ) q WHERE rn > 1
  ),
  not_top_for_document AS (
    SELECT * FROM (
      SELECT
        t.*,
        ROW_NUMBER() OVER (
          PARTITION BY t.document_id
          ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
        ) AS rn
      FROM top_per_transaction t
    ) q WHERE rn > 1
  ),

  /* matched -> reconciliation_activity (if enabled) */
  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        AS run_id,
      log_run_unix_time AS time,
      lb.company_id, i.transaction_id, i.document_id,
      'matched'::text   AS action,
      NULL::text        AS reason,
      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
  ),

  /* skipped -> reconciliations_activity (if enabled) */
  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 (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_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 (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  /* pair cache: matched */
  cache_upsert_matched AS (
    INSERT INTO public.reconciliations_pair_cache_id (
      company_id, transaction_id, document_id, last_action, last_confidence, last_seen_unix
    )
    SELECT
      lb.company_id, i.transaction_id, i.document_id,
      'matched', lb.confidence_percent, log_run_unix_time
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    ON CONFLICT (transaction_id, document_id) DO UPDATE
    SET last_action     = EXCLUDED.last_action,
        last_confidence = EXCLUDED.last_confidence,
        last_seen_unix  = EXCLUDED.last_seen_unix
    RETURNING 1
  ),

  /* pair cache: skipped */
  cache_upsert_skipped AS (
    INSERT INTO public.reconciliations_pair_cache_id (
      company_id, transaction_id, document_id, last_action, last_confidence, last_seen_unix
    )
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM not_top_for_transaction x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM not_top_for_document x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_min_confidence
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    ON CONFLICT (transaction_id, document_id) DO UPDATE
    SET last_action     = EXCLUDED.last_action,
        last_confidence = EXCLUDED.last_confidence,
        last_seen_unix  = EXCLUDED.last_seen_unix
    RETURNING 1
  ),

  /* final counts for return and logging */
  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM top_per_transaction)          AS c_top_per_transaction,
      (SELECT COUNT(*) FROM top_per_document)             AS c_top_per_document,
      (SELECT COUNT(*) FROM final_candidates)             AS c_passed_confidence,
      (SELECT COUNT(*) FROM base_candidates)              AS c_base_candidates,
      (SELECT COUNT(*) FROM inserted_reconciliations)     AS c_inserted
  )
  SELECT
    c_top_per_transaction, c_top_per_document, c_passed_confidence,
    c_base_candidates, c_inserted
  INTO
    summary_top_per_transaction_count, summary_top_per_document_count, summary_passed_confidence_count,
    v_candidates_count, v_inserted_count
  FROM final_counts;

  /* run_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,
      'use_pair_cache', config_use_pair_cache
    )
  );

  RETURN jsonb_build_object(
    'msg',               message_text,
    '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;

  BEGIN
    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', SQLERRM,
      p_company_id, NULL, NULL, NULL,
      NULL, NULL, NULL, elapsed_ms,
      jsonb_build_object('hint','see server logs or activity tables')
    );
  EXCEPTION WHEN OTHERS THEN
    NULL;
  END;

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

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

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

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

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

create or replace function athena_reconcile_general_v1_7(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  message_text text := 'success';
  engine_version text := '1.7';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean; -- reserved
  config_log_activity boolean;
  config_use_pair_cache boolean := false;   -- toggle cache pruning

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;

  -- run meta
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time bigint := EXTRACT(EPOCH FROM now())::bigint;

  -- for reconciliations_logs
  v_candidates_count bigint := 0;
  v_inserted_count bigint := 0;

  -- advisory lock
  lock_acquired boolean := true;
BEGIN
  -- 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 (
    log_run_id, engine_version, 'run_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'reconciliation_config_id', p_reconciliation_config_id,
        'max_transactions', derived_max_transactions,
        'seed_cap_per_criterion', derived_seed_cap_per_criterion
      )
    )
  );

  /* 0) single-company run guard */
  lock_acquired := pg_try_advisory_xact_lock(
                     hashtext('athena_reconcile_general_v1_7'),
                     COALESCE(hashtext(COALESCE(p_company_id::text,'__all__')), 0)
                   );
  IF NOT lock_acquired THEN
    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', 'another run in progress',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms,
      jsonb_build_object('note','lock not acquired')
    );

    RETURN jsonb_build_object(
      'msg','another run in progress','time_ms',elapsed_ms,
      'top_per_tx',0,'top_per_doc',0,'passed_confidence',0
    );
  END IF;

  /* 1) load configuration (includes use_pair_cache) */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity, COALESCE(use_pair_cache,false)
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity, config_use_pair_cache
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    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', 'error', 'reconciliations_config not found',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms, NULL
    );

    RETURN jsonb_build_object('msg','reconciliations_config not found','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* 1a) derive configuration */
  derived_amount_tolerance_fraction :=
    CASE WHEN config_amount_tolerance >= 1
         THEN config_amount_tolerance / 100.0
         ELSE config_amount_tolerance
    END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

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

  /* 2) resolve weights */
  weights_json :=
    COALESCE((
      WITH candidate_weights AS (
        SELECT w.*
        FROM public.reconciliations_weight_registry w
        WHERE w.active
          AND (
               (p_company_id IS NULL AND w.company_id IS NULL AND w.merchant_id IS NULL)
            OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR (w.company_id IS NULL AND w.merchant_id IS NULL)))
          )
      ),
      picked_weights AS (
        SELECT DISTINCT ON (weight_key) weight_key, weight_value, company_id
        FROM candidate_weights
        ORDER BY weight_key, CASE WHEN company_id IS NULL THEN 1 ELSE 0 END
      )
      SELECT jsonb_object_agg(weight_key, weight_value) FROM picked_weights
    ), '{}'::jsonb);

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

  /* 3) pipeline + inserts + activity logs + pair cache (positive-only) */
  WITH
  document_scope AS (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),

  transactions_scope AS (
    SELECT
      t.transaction_id,
      t.company_id,                              -- text
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      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.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 derived_max_transactions
  ),

  tx_with_cache AS (
    SELECT DISTINCT t.transaction_id
    FROM transactions_scope t
    JOIN public.reconciliations_pair_cache_id rpc
      ON rpc.transaction_id = t.transaction_id
  ),

  candidate_pairs_cached AS (
    SELECT
      t.transaction_id,
      d.document_id
    FROM transactions_scope t
    JOIN tx_with_cache twc
      ON twc.transaction_id = t.transaction_id
    JOIN public.reconciliations_pair_cache_id rpc
      ON rpc.transaction_id = t.transaction_id
    JOIN document_scope d
      ON d.document_id = rpc.document_id
     AND d.company_id = t.company_id::uuid
    WHERE
      config_use_pair_cache
      AND (
           (d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
        OR (d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id)
        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 (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 * derived_amount_tolerance_fraction
               )
           ))
      )
  ),

  candidate_pairs_uncached 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 IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR
          (d.order_id   IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id   = t.order_id)
          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
          (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 * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 6)
    ) d ON TRUE
    WHERE
      (NOT config_use_pair_cache)
      OR (config_use_pair_cache AND NOT EXISTS (
            SELECT 1 FROM tx_with_cache twc WHERE twc.transaction_id = t.transaction_id
          ))
  ),

  candidate_pairs AS (
    SELECT * FROM candidate_pairs_cached
    UNION ALL
    SELECT * FROM candidate_pairs_uncached
  ),

  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 AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      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.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name,
      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.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
         AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,

      COALESCE(
        (a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
         AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,

      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,

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

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

      (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 * derived_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 * derived_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.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 * derived_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((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),

  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total) / NULLIF(m.document_total * derived_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.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::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.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 weights_max_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)
                        / weights_max_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
  ),

  /* thresholds and ranking */
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= 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
  ),
  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.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  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
  ),

  /* single membership gate */
  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, 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.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      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,
      e.confidence_percent,
      log_run_unix_time,
      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.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_max_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
  ),

  /* base for activity logs */
  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
  ),

  /* losers for logging */
  not_top_for_transaction AS (
    SELECT * FROM (
      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 rn
      FROM base_candidates b
    ) q WHERE rn > 1
  ),
  not_top_for_document AS (
    SELECT * FROM (
      SELECT
        t.*,
        ROW_NUMBER() OVER (
          PARTITION BY t.document_id
          ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
        ) AS rn
      FROM top_per_transaction t
    ) q WHERE rn > 1
  ),

  /* matched -> reconciliation_activity (if enabled) */
  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        AS run_id,
      log_run_unix_time AS time,
      lb.company_id, i.transaction_id, i.document_id,
      'matched'::text   AS action,
      NULL::text        AS reason,
      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
  ),

  /* skipped -> reconciliations_activity (if enabled) */
  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
    RETURNING 1
  ),

  /* pair cache: POSITIVE ONLY (matched) */
  cache_upsert_matched AS (
    INSERT INTO public.reconciliations_pair_cache_id (
      company_id, transaction_id, document_id, last_action, last_confidence, last_seen_unix
    )
    SELECT
      lb.company_id, i.transaction_id, i.document_id,
      'matched', lb.confidence_percent, log_run_unix_time
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    ON CONFLICT (transaction_id, document_id) DO UPDATE
    SET last_action     = EXCLUDED.last_action,
        last_confidence = EXCLUDED.last_confidence,
        last_seen_unix  = EXCLUDED.last_seen_unix
    RETURNING 1
  ),

  /* final counts for return and logging */
  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM base_candidates)          AS c_base_candidates,
      (SELECT COUNT(*) FROM inserted_reconciliations) AS c_inserted,
      (SELECT COUNT(*) FROM top_per_transaction)      AS c_top_per_transaction,
      (SELECT COUNT(*) FROM top_per_document)         AS c_top_per_document,
      (SELECT COUNT(*) FROM final_candidates)         AS c_passed_confidence
  )
  SELECT
    c_top_per_transaction, c_top_per_document, c_passed_confidence,
    c_base_candidates, c_inserted
  INTO
    summary_top_per_transaction_count, summary_top_per_document_count, summary_passed_confidence_count,
    v_candidates_count, v_inserted_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',               message_text,
    '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;

  BEGIN
    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', SQLERRM,
      p_company_id, NULL, NULL, NULL,
      NULL, NULL, NULL, elapsed_ms,
      jsonb_build_object('hint','see server logs or activity tables')
    );
  EXCEPTION WHEN OTHERS THEN
    NULL;
  END;

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

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

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

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

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

create or replace function athena_reconcile_general_v1_9(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  -- Status and versioning
  status_message              text := 'success';
  engine_version              text := '1.9';

  -- Timing
  start_time                  timestamptz := clock_timestamp();
  elapsed_ms                  bigint := 0;

  -- Configuration from DB
  config_amount_tolerance           numeric;  -- percentage or fraction
  config_date_window_days           numeric;
  config_min_confidence             numeric;  -- may be 0–1 or 0–100
  config_allow_document_duplicates  boolean;  -- reserved
  config_log_activity               boolean;

  -- Derived configuration
  derived_amount_tolerance_fraction numeric;  -- always a fraction 0–1
  derived_date_window_seconds       bigint;   -- days -> seconds
  seed_cap_per_match_type           int := 10; -- per-criterion seed cap
  max_transactions_to_scan          int := 20000;

  -- Weight registry
  weights_json                jsonb := '{}'::jsonb;
  weights_total_possible      numeric := 0;     -- sum of all weights

  -- Summary/result counts
  summary_top_per_transaction_count  bigint := 0;
  summary_top_per_document_count     bigint := 0;
  summary_passed_confidence_count    bigint := 0;
  v_inserted_count                   bigint := 0;
  v_candidates_count                 bigint := 0;

  -- Run metadata
  log_run_id                   uuid := gen_random_uuid();
  log_run_unix_time            bigint := EXTRACT(EPOCH FROM now())::bigint;

  -- Error diagnostics
  err_state   text;
  err_msg     text;
  err_detail  text;
  err_hint    text;
  err_context text;

BEGIN

  -- Load configuration
  SELECT amount_tolerance,
         date_window_days,
         min_confidence,
         allow_doc_duplicates,
         log_activity
  INTO   config_amount_tolerance,
         config_date_window_days,
         config_min_confidence,
         config_allow_document_duplicates,
         config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

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

  -- Derive configuration
  derived_amount_tolerance_fraction := CASE
    WHEN config_amount_tolerance >= 1 THEN config_amount_tolerance / 100.0
    ELSE config_amount_tolerance
  END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

  IF config_min_confidence IS NULL THEN
    config_min_confidence := 0;
  END IF;
  IF config_min_confidence <= 1 THEN
    config_min_confidence := config_min_confidence * 100.0; -- normalize to percent
  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_weights AS (
    SELECT DISTINCT ON (k_norm)
           k_norm, v_norm
    FROM candidate_weights
    ORDER BY k_norm, (company_id IS NULL) -- company-specific first
  ),
  wj AS (
    SELECT jsonb_object_agg(k_norm, v_norm) AS j
    FROM picked_weights
  )
  SELECT COALESCE(j, '{}'::jsonb)
  INTO weights_json
  FROM wj;

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

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

  -- Log run_start
  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_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', config_log_activity,
        'allow_doc_dups', config_allow_document_duplicates,
        'amount_tol_pct', derived_amount_tolerance_fraction,
        'min_confidence', config_min_confidence,
        'date_window_days', config_date_window_days,
        'max_tx_seed', max_transactions_to_scan,
        'seed_cap_per_criterion', seed_cap_per_match_type
      ),
      'weights_json', weights_json,
      'weights_max_possible', weights_total_possible
    )
  );

  -- Main pipeline
  WITH
  document_scope AS MATERIALIZED (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    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,      -- text in source table
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      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.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 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 IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR (d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id)
          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 (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 * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (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 AS transaction_invoice_number,
      d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id,
      d.order_id AS document_order_id,
      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.name_consumer AS transaction_customer_name,
      d.customer_name AS document_customer_name,
      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.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
                AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,
      COALESCE((a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
                AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,
      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,
      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= derived_date_window_seconds, FALSE) AS date_match,

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

      (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 * derived_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 * derived_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.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 * derived_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((weights_json->>'amount_total')::numeric, 0)   AS amount_weight,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS invoice_weight,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS order_weight,
      COALESCE((weights_json->>'date')::numeric, 0)           AS date_weight,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS currency_weight,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS merchant_weight,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS customer_weight
  ),

  scored_components AS (
    SELECT
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      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 * derived_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.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 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)
                / 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
  ),

  -- Thresholds and ranking
  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 < config_min_confidence
  ),
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= 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.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_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
  ),

  -- Membership gate against any prior reconciliation
  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, 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.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      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,
      log_run_unix_time,
      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.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
  ),

  -- Optional activity logging
  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;

  -- Timing end and run_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
  -- Error handling
  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_v1_9(unknown, unknown) owner to postgres;

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

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

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

create or replace function add_merchant_iban(body jsonb) returns merchant_ibans
    security definer
    SET search_path = public
    language plpgsql
as
$$
DECLARE v_slug_in text := nullif(
        trim(
            both
            from body->>'slug'
        ),
        ''
    );
v_company_name text := nullif(
    trim(
        both
        from body->>'company_name'
    ),
    ''
);
v_account_holder text := nullif(
    trim(
        both
        from body->>'account_holder'
    ),
    ''
);
v_name text := coalesce(
    v_company_name,
    v_account_holder,
    nullif(
        trim(
            both
            from body->>'name'
        ),
        ''
    )
);
v_icon text := nullif(body->>'icon', '');
v_email text := nullif(body->>'email', '');
v_website text := nullif(body->>'website', '');
v_description text := nullif(body->>'description', '');
v_billing_postal text := nullif(body->>'billing_postal_code', '');
v_billing_line_1 text := nullif(body->>'billing_address_line_1', '');
v_billing_line_2 text := nullif(body->>'billing_address_line_2', '');
v_language text := nullif(body->>'language', '');
v_industry text := nullif(body->>'industry', '');
v_phone text := nullif(body->>'phone_number', '');
v_public boolean := coalesce((body->>'public')::boolean, true);
v_vat_id text := nullif(body->>'vat_id', '');
v_billing_city text := nullif(body->>'billing_city', '');
v_billing_state text := nullif(body->>'billing_state', '');
v_billing_province text := nullif(body->>'billing_province', '');
v_company_number text := nullif(body->>'company_number', '');
v_company_num_type text := nullif(body->>'company_number_type', '');
v_country_code text := nullif(body->>'country_code', '');
v_iban_raw text := body->>'iban';
v_iban text := upper(
    regexp_replace(coalesce(body->>'iban', ''), '\s', '', 'g')
);
v_bank text := nullif(body->>'bank', '');
v_bic text := nullif(body->>'bic', '');
v_iban_desc text := nullif(body->>'iban_description', '');
v_iban_country_code text := nullif(body->>'iban_country_code', '');
v_slug text;
v_base_slug text;
v_try_slug text;
v_suffix int := 1;
v_merchant_id uuid;
v_row public.merchant_ibans;
v_has_unaccent boolean := false;
BEGIN IF v_iban_raw IS NULL
OR v_iban = '' THEN RAISE EXCEPTION 'iban is required' USING ERRCODE = '22023';
END IF;
IF v_slug_in IS NULL
AND (
    v_name IS NULL
    OR v_name = ''
) THEN RAISE EXCEPTION 'company_name or account_holder (or name) is required to auto-generate slug' USING ERRCODE = '22023';
END IF;
SELECT EXISTS (
        SELECT 1
        FROM pg_extension
        WHERE extname = 'unaccent'
    ) INTO v_has_unaccent;
v_base_slug := coalesce(v_slug_in, lower(coalesce(v_name, 'merchant')));
IF v_has_unaccent THEN v_base_slug := unaccent(v_base_slug);
END IF;
v_base_slug := regexp_replace(v_base_slug, '[^a-z0-9]+', '-', 'g');
v_base_slug := regexp_replace(v_base_slug, '(^-+)|(-+$)', '', 'g');
IF v_base_slug IS NULL
OR v_base_slug = '' THEN v_base_slug := 'merchant';
END IF;
v_try_slug := v_base_slug;
v_merchant_id := NULL;
SELECT m.merchant_id INTO v_merchant_id
FROM public.global_merchants_registry m
WHERE m.slug = v_try_slug;
v_slug := v_try_slug;
IF v_merchant_id IS NULL THEN
INSERT INTO public.global_merchants_registry (
        name,
        icon,
        email,
        website,
        description,
        billing_postal_code,
        billing_address_line_1,
        billing_address_line_2,
        language,
        industry,
        phone_number,
        slug,
        enabled,
        country_code,
        public,
        vat_id,
        billing_city,
        billing_state,
        billing_province,
        company_number,
        company_number_type
    )
VALUES (
        v_name,
        v_icon,
        v_email,
        v_website,
        v_description,
        v_billing_postal,
        v_billing_line_1,
        v_billing_line_2,
        v_language,
        v_industry,
        v_phone,
        v_slug,
        true,
        v_country_code,
        v_public,
        v_vat_id,
        v_billing_city,
        v_billing_state,
        v_billing_province,
        v_company_number,
        v_company_num_type
    )
RETURNING merchant_id INTO v_merchant_id;
END IF;
INSERT INTO public.merchant_ibans (
        merchant_id,
        iban,
        country_code,
        bank,
        description,
        bic,
        slug
    )
VALUES (
        v_merchant_id,
        v_iban,
        coalesce(v_iban_country_code, v_country_code),
        v_bank,
        coalesce(v_iban_desc, v_description),
        v_bic,
        v_slug
    )
RETURNING * INTO v_row;
RETURN v_row;
EXCEPTION
WHEN unique_violation THEN IF SQLERRM LIKE '%merchant_ibans_iban_key%' THEN RAISE EXCEPTION 'IBAN % already exists',
v_iban USING ERRCODE = '23505';
ELSE RAISE;
END IF;
WHEN others THEN RAISE;
END;
$$;

alter function add_merchant_iban(unknown) owner to postgres;

grant execute on function add_merchant_iban(unknown) to anon;

grant execute on function add_merchant_iban(unknown) to authenticated;

grant execute on function add_merchant_iban(unknown) to service_role;

create or replace function add_merchant_iban(slug text DEFAULT NULL::text, company_name text DEFAULT NULL::text, iban text DEFAULT NULL::text, bank text DEFAULT NULL::text, bic text DEFAULT NULL::text, iban_description text DEFAULT NULL::text, country_code text DEFAULT NULL::text, email text DEFAULT NULL::text) returns merchant_ibans
    language plpgsql
as
$$
DECLARE
  -- local variables
  v_slug text := slug;
  v_name text := company_name;
  v_iban text := upper(regexp_replace(coalesce(iban, ''), '\s', '', 'g'));
  v_row public.merchant_ibans;
  v_merchant_id uuid;
BEGIN
  IF v_iban IS NULL OR v_iban = '' THEN
    RAISE EXCEPTION 'iban is required' USING ERRCODE = '22023';
  END IF;

  -- create/find merchant
  SELECT m.merchant_id INTO v_merchant_id
  FROM public.global_merchants_registry m
  WHERE m.slug = v_slug;

  IF v_merchant_id IS NULL THEN
    INSERT INTO public.global_merchants_registry (name, slug, email, country_code)
    VALUES (v_name, v_slug, email, country_code)
    RETURNING merchant_id INTO v_merchant_id;
  END IF;

  -- insert iban
  INSERT INTO public.merchant_ibans (merchant_id, slug, iban, country_code, bank, description, bic)
  VALUES (v_merchant_id, v_slug, v_iban, country_code, bank, iban_description, bic)
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;

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

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

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

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

create or replace function find_merchant_by_slug(slug text) returns jsonb
    security definer
    SET search_path = public
    language plpgsql
as
$$
DECLARE
  v_result jsonb;
  v_merchant public.global_merchants_registry;
BEGIN
  -- Fetch merchant by slug
  SELECT m.* INTO v_merchant
  FROM public.global_merchants_registry m
  WHERE m.slug = slug;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Merchant with slug % not found', slug USING ERRCODE = 'P0002';
  END IF;

  -- Build JSON result: merchant + ibans
  SELECT jsonb_build_object(
    'merchant', to_jsonb(v_merchant),
    'ibans', (
      SELECT jsonb_agg(to_jsonb(i))
      FROM public.merchant_ibans i
      WHERE i.slug = slug
      ORDER BY i.created_at
    )
  )
  INTO v_result;

  RETURN v_result;
END;
$$;

alter function find_merchant_by_slug(unknown) owner to postgres;

grant execute on function find_merchant_by_slug(unknown) to anon;

grant execute on function find_merchant_by_slug(unknown) to authenticated;

grant execute on function find_merchant_by_slug(unknown) to service_role;

create or replace function athena_reconcile_general_v2_0(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  status_message text := 'success';
  engine_version text := '2.0';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- config from DB
  config_amount_tolerance           numeric;
  config_date_window_days           numeric;
  config_min_confidence             numeric;
  config_allow_document_duplicates  boolean;
  config_log_activity               boolean;

  -- derived
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds       bigint;
  seed_cap_per_match_type           int := 10;
  max_transactions_to_scan          int := 20000;

  -- weights
  weights_json           jsonb := '{}'::jsonb;
  weights_total_possible numeric := 0;

  -- counters
  summary_top_per_transaction_count  bigint := 0;
  summary_top_per_document_count     bigint := 0;
  summary_passed_confidence_count    bigint := 0;
  v_inserted_count                   bigint := 0;
  v_candidates_count                 bigint := 0;

  -- run meta
  log_run_id        uuid := gen_random_uuid();
  log_run_unix_time bigint := EXTRACT(EPOCH FROM now())::bigint;

  -- optional rule: direction-aware matching for names/IBAN
  directional_matching boolean := TRUE;  -- set FALSE to allow name/IBAN matching regardless of in/out

  -- errors
  err_state   text;
  err_msg     text;
  err_detail  text;
  err_hint    text;
  err_context text;

BEGIN
  -- config load
  SELECT amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

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

  -- derive
  derived_amount_tolerance_fraction := CASE
    WHEN config_amount_tolerance >= 1 THEN config_amount_tolerance / 100.0
    ELSE config_amount_tolerance
  END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

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

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

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

  IF weights_total_possible = 0 THEN
    weights_json := jsonb_build_object(
      'amount_total',1,'invoice_number',1,'order_number',1,'date',1,'currency',1,'merchant_id',1,'customer_name',1
    );
    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 (
    log_run_id, 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', config_log_activity,
        'allow_doc_dups', config_allow_document_duplicates,
        'amount_tol_pct', derived_amount_tolerance_fraction,
        'min_confidence', config_min_confidence,
        'date_window_days', config_date_window_days,
        'max_tx_seed', max_transactions_to_scan,
        'seed_cap_per_criterion', seed_cap_per_match_type,
        'directional_matching', directional_matching
      ),
      'weights_json', weights_json,
      'weights_max_possible', 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,
      NULLIF(d.order_id, '')       AS order_id,

      -- derive document_date_unix without external function
      CASE
        WHEN d.document_date_unix IS NOT NULL THEN d.document_date_unix
        WHEN d.document_date ~ '^\d{4}-[A-Za-z]+-\d{2}$' THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'YYYY-Month-DD'))::bigint
        WHEN d.document_date ~ '^\d{2}-[A-Za-z]+-\d{4}$' THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'DD-Month-YYYY'))::bigint
        WHEN d.document_date ~ '^\d{4}-\d{2}-\d{2}$'     THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'YYYY-MM-DD'))::bigint
        WHEN d.document_date ~ '^\d{2}-\d{2}-\d{4}$'     THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'DD-MM-YYYY'))::bigint
        ELSE NULL
      END AS document_date_unix,

      d.merchant_id,
      d.customer_name,

      -- normalized names + IBAN
      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(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,

      -- direction flags
      (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,

      -- normalized names + IBAN
      NULLIF(public.norm_core_ns(
        COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
      ), '') AS tx_merchant_ns,

      -- build customer from multiple fields
      NULLIF(public.norm_text_ns(
        COALESCE(
          t.name_consumer,
          t.account_holder_name,               -- own account holder name
          t.account_holder_name_counterparty   -- counterparty name if mapped here
        )
      ), '') 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.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 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 keys
          (d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR (d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id)
          OR (d.currency = t.currency AND d.merchant_id   = t.merchant_id)
          OR (d.currency = t.currency AND d.customer_name = t.name_consumer)

          -- name seeding with date window; direction optional
          OR (((NOT directional_matching) OR t.tx_is_out) AND d.currency = t.currency
              AND d.document_date_unix IS NOT NULL
              AND ABS(d.document_date_unix - t.completed_date) <= derived_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 directional_matching) OR t.tx_is_in) AND d.currency = t.currency
              AND d.document_date_unix IS NOT NULL
              AND ABS(d.document_date_unix - t.completed_date) <= derived_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 seeding for outgoing normally; direction optional
          OR (((NOT directional_matching) OR t.tx_is_out) AND d.currency = t.currency
              AND d.document_date_unix IS NOT NULL
              AND ABS(d.document_date_unix - t.completed_date) <= derived_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)

          -- amount path
          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 * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   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 (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 AS transaction_invoice_number,
      d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id,
      d.order_id AS document_order_id,
      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.name_consumer AS transaction_customer_name,
      d.customer_name 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,

      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.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
                AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,
      COALESCE((a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
                AND a.transaction_order_id = a.document_order_id), FALSE) AS order_number_match,
      COALESCE(a.transaction_currency  = a.document_currency, FALSE) AS currency_match,

      -- merchant: id always; name/IBAN gated by direction if enabled
      (
        (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 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 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,

      -- customer: gated by direction if enabled
      (
        ((NOT 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) <= derived_date_window_seconds, FALSE) AS date_match,

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

      (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 * derived_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 * derived_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.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 * derived_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((weights_json->>'amount_total')::numeric, 0)   AS amount_weight,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS invoice_weight,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS order_weight,
      COALESCE((weights_json->>'date')::numeric, 0)           AS date_weight,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS currency_weight,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS merchant_weight,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS customer_weight
  ),

  scored_components AS (
    SELECT
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      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 * derived_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.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 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)
                / 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
  ),

  -- thresholds and ranking
  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 < config_min_confidence
  ),
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE signals_matched_count >= 1 AND confidence_percent >= 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.document_id
        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.transaction_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, 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.transaction_invoice_number, a.document_invoice_number,
        a.transaction_order_id, a.document_order_id,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name
      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,
      log_run_unix_time, 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.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 (
    SELECT t.transaction_id, d_sub.document_id, 'window_only' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s5_merchant_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s6_customer_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s7_iban_plus_signal
      UNION ALL SELECT DISTINCT transaction_id FROM s8_merchant_id_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s9_merchant_ns_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s10_customer_ns_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s11_iban_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s12_window_amount_exact
      UNION ALL SELECT DISTINCT transaction_id FROM s13_window_amount_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
        AND ABS(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      ORDER BY ABS(d.document_date_unix - t.completed_date)
      LIMIT v_cap_window_only
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),

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

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

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

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

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

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

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

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

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

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

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

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

      t.tx_iban_beneficiary_ns,
      t.tx_iban_counterparty_ns,
      d.doc_iban_ns,

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

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

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

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

      (
        CASE
          WHEN v_directional_matching THEN
            ( a.tx_is_out AND (
                 (a.transaction_merchant_id IS NOT NULL AND a.document_merchant_id IS NOT NULL
                  AND a.transaction_merchant_id = a.document_merchant_id)
              OR (a.tx_merchant_ns IS NOT NULL AND a.doc_merchant_ns IS NOT NULL AND (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 OR position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 OR position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              OR (a.tx_iban_counterparty_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
                  AND a.tx_iban_counterparty_ns = a.doc_iban_ns)
            ))
          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,

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

      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,

      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.*,

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

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

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

        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 COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb) FROM seed_counts_total)    AS seed_counts_total_json,
      (SELECT COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb) FROM seed_counts_distinct) AS seed_counts_distinct_json,
      (SELECT COUNT(*) FROM transactions_scope)       AS total_tx_count,
      (SELECT COUNT(*) FROM document_scope)           AS total_doc_count
  )
  SELECT
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  INTO
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  FROM final_counts;

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

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

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

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

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

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

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

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

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

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

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

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

  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;
  v_cap_broad                 int := 10;
  v_cap_window_only           int := 3;
  v_cap_iban_exact            int := 5;
  v_iban_exact_window_seconds bigint;
  v_max_transactions_to_scan  int := 20000;

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

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

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

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

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

  -- matching mode
  v_directional_matching boolean := TRUE;

  -- errors
  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;
BEGIN
  -- 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 (
    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 (
    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 (
    SELECT t.transaction_id, d_sub.document_id, 'window_only' AS seed
    FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id FROM s1_invoice
      UNION ALL SELECT DISTINCT transaction_id FROM s2_order
      UNION ALL SELECT DISTINCT transaction_id FROM s3_iban_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s4_merchant_id_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s5_merchant_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s6_customer_ns_exact_amount
      UNION ALL SELECT DISTINCT transaction_id FROM s7_iban_plus_signal
      UNION ALL SELECT DISTINCT transaction_id FROM s8_merchant_id_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s9_merchant_ns_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s10_customer_ns_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s11_iban_tol
      UNION ALL SELECT DISTINCT transaction_id FROM s12_window_amount_exact
      UNION ALL SELECT DISTINCT transaction_id FROM s13_window_amount_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL AND t.completed_date IS NOT NULL
        AND ABS(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      ORDER BY ABS(d.document_date_unix - t.completed_date)
      LIMIT v_cap_window_only
    ) d_sub ON TRUE
    WHERE done.transaction_id IS NULL
  ),

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

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

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

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

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

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

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

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

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

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

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

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

      t.tx_iban_beneficiary_ns,
      t.tx_iban_counterparty_ns,
      d.doc_iban_ns,

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

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

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

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

      (
        CASE
          WHEN v_directional_matching THEN
            ( a.tx_is_out AND (
                 (a.transaction_merchant_id IS NOT NULL AND a.document_merchant_id IS NOT NULL
                  AND a.transaction_merchant_id = a.document_merchant_id)
              OR (a.tx_merchant_ns IS NOT NULL AND a.doc_merchant_ns IS NOT NULL AND (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 OR position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 OR position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              OR (a.tx_iban_counterparty_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
                  AND a.tx_iban_counterparty_ns = a.doc_iban_ns)
            ))
          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,

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

      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,

      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.*,

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

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

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

        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 COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb) FROM seed_counts_total)    AS seed_counts_total_json,
      (SELECT COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb) FROM seed_counts_distinct) AS seed_counts_distinct_json,
      (SELECT COUNT(*) FROM transactions_scope)       AS total_tx_count,
      (SELECT COUNT(*) FROM document_scope)           AS total_doc_count
  )
  SELECT
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  INTO
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  FROM final_counts;

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

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

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

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

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

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

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

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

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

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

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

create or replace function norm_iban(iban_in text) returns text
    immutable
    language plpgsql
as
$$
DECLARE
  raw  text;
  cc   text;
  chk  text;
  rest text;
  acct text;
BEGIN
  IF iban_in IS NULL THEN RETURN NULL; END IF;

  raw := upper(regexp_replace(iban_in, '[^A-Z0-9]', '', 'g'));
  IF length(raw) < 4 THEN RETURN raw; END IF;

  cc   := substr(raw, 1, 2);
  chk  := translate(substr(raw, 3, 2), 'OQIGLSTBZ', '001115782'); -- fix digits only
  rest := substr(raw, 5);

  IF cc = 'NL' THEN
    IF length(rest) >= 14 THEN
      acct := translate(substr(rest, 5), 'OQIGLSTBZ', '001115782'); -- pos 9+
      rest := substr(rest, 1, 4) || acct;                           -- keep bank code letters
    END IF;
  ELSIF cc IN ('IE','GB') THEN
    IF length(rest) >= 4 THEN
      acct := translate(substr(rest, 5), 'OQIGLSTBZ', '001115782'); -- pos 9+
      rest := substr(rest, 1, 4) || acct;
    END IF;
  ELSIF cc IN ('DE','BE') THEN
    rest := translate(rest, 'OQIGLSTBZ', '001115782');               -- all digits after 4
  END IF;

  RETURN cc || chk || rest;
END;
$$;

alter function norm_iban(unknown) owner to postgres;

grant execute on function norm_iban(unknown) to anon;

grant execute on function norm_iban(unknown) to authenticated;

grant execute on function norm_iban(unknown) to service_role;

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

  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;
  v_cap_broad                 int := 10;
  v_cap_window_only           int := 3;
  v_cap_iban_exact            int := 5;         -- still cap IBAN-exact
  v_iban_exact_window_seconds bigint;           -- retained but unused in M
  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;
  v_iban_exact_window_seconds := LEAST(v_date_window_seconds, 14 * 86400)::bigint; -- retained for compatibility

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

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

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

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

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

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

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

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

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

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

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

  /* ====================== STAGED SEEDING (specific → broad) ====================== */

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      a.tx_is_in, a.tx_is_out,

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

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

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

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

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

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

      m.tx_is_in, m.tx_is_out,

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

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

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN
            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_applicability AS (
    SELECT
      s.*,

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

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

    FROM scored_components s
  ),

  scored_with_confidence AS (
    SELECT
      s.*,

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

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

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

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

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

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

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

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

  allowed_for_insert AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE  NOT EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       AND NOT EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),

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

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

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

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

        a.tx_is_in, a.tx_is_out
      FROM ts_base t
      JOIN final_candidates d ON d.transaction_id = t.transaction_id AND d.document_id = t.document_id
      JOIN attributes a       ON a.transaction_id = d.transaction_id AND a.document_id = d.document_id
    )
    SELECT
      e.transaction_id, e.document_id, e.document_company_id,
      COALESCE(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      EXTRACT(YEAR FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          SELECT json_agg(s) FROM unnest(ARRAY[
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]) s WHERE s IS NOT NULL
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  COALESCE(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_order',     e.tx_order_norm,
        'doc_order',    COALESCE(e.doc_order_raw, e.doc_order_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_iban_beneficiary',        e.tx_iban_beneficiary_ns,
        'tx_iban_counterparty',       e.tx_iban_counterparty_ns,
        'doc_iban',                   e.doc_iban_ns,
        'tx_iban_beneficiary_core',   e.tx_iban_beneficiary_core_ns,
        'tx_iban_counterparty_core',  e.tx_iban_counterparty_core_ns,
        'doc_iban_core',              e.doc_iban_core_ns,
        'tx_is_in',    e.tx_is_in,
        'tx_is_out',   e.tx_is_out,
        'direction',   CASE WHEN e.tx_is_in THEN 'incoming' WHEN e.tx_is_out THEN 'outgoing' ELSE 'unknown' END
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

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

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

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

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

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

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

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

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

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

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

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

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

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

create or replace function norm_iban_core(iban_in text) returns text
    immutable
    language plpgsql
as
$$
DECLARE
  raw  text;
  cc   text;
  rest text;   -- positions 5+
  acct text;
BEGIN
  IF iban_in IS NULL THEN RETURN NULL; END IF;

  raw := upper(regexp_replace(iban_in, '[^A-Z0-9]', '', 'g'));
  IF length(raw) < 5 THEN RETURN raw; END IF;

  cc   := substr(raw, 1, 2);
  rest := substr(raw, 5);  -- drop check digits entirely

  IF cc = 'NL' THEN
    IF length(rest) >= 14 THEN
      acct := translate(substr(rest, 5), 'OQIGLSTBZ', '001115782'); -- pos 9+
      rest := substr(rest, 1, 4) || acct;
    END IF;
  ELSIF cc IN ('IE','GB') THEN
    IF length(rest) >= 4 THEN
      acct := translate(substr(rest, 5), 'OQIGLSTBZ', '001115782'); -- pos 9+
      rest := substr(rest, 1, 4) || acct;
    END IF;
  ELSIF cc IN ('DE','BE') THEN
    rest := translate(rest, 'OQIGLSTBZ', '001115782');               -- all digits after 4
  END IF;

  RETURN cc || rest;
END;
$$;

alter function norm_iban_core(unknown) owner to postgres;

grant execute on function norm_iban_core(unknown) to anon;

grant execute on function norm_iban_core(unknown) to authenticated;

grant execute on function norm_iban_core(unknown) to service_role;

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

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

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

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

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

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

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

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

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

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

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

  -- matching mode
  v_directional_matching boolean := TRUE;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      a.tx_is_in, a.tx_is_out,

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

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

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

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

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

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

      m.tx_is_in, m.tx_is_out,

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

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

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

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

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

      m.date_within_cap AS date_match,

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

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

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

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

  scored_with_applicability AS (
    SELECT
      s.*,

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

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

    FROM scored_components s
  ),

  scored_with_confidence AS (
    SELECT
      s.*,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  -- matching mode
  v_directional_matching boolean := TRUE;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      a.tx_is_in, a.tx_is_out,

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

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

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

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

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

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

      m.tx_is_in, m.tx_is_out,

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

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

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

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

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

      m.date_within_cap AS date_match,

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

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

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

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

  scored_with_applicability AS (
    SELECT
      s.*,

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

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

    FROM scored_components s
  ),

  scored_with_confidence AS (
    SELECT
      s.*,

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

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

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

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

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

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

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

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

  allowed_for_insert AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE  NOT EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       AND NOT EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),

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

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

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

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

        a.tx_is_in, a.tx_is_out
      FROM ts_base t
      JOIN final_candidates d ON d.transaction_id = t.transaction_id AND d.document_id = t.document_id
      JOIN attributes a       ON a.transaction_id = d.transaction_id AND a.document_id = d.document_id
    )
    SELECT
      e.transaction_id, e.document_id, e.document_company_id,
      COALESCE(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      EXTRACT(YEAR FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          SELECT json_agg(s) FROM unnest(ARRAY[
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]) s WHERE s IS NOT NULL
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  COALESCE(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_order',     e.tx_order_norm,
        'doc_order',    COALESCE(e.doc_order_raw, e.doc_order_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_iban_beneficiary',        e.tx_iban_beneficiary_ns,
        'tx_iban_counterparty',       e.tx_iban_counterparty_ns,
        'doc_iban',                   e.doc_iban_ns,
        'tx_iban_beneficiary_core',   e.tx_iban_beneficiary_core_ns,
        'tx_iban_counterparty_core',  e.tx_iban_counterparty_core_ns,
        'doc_iban_core',              e.doc_iban_core_ns,
        'tx_is_in',    e.tx_is_in,
        'tx_is_out',   e.tx_is_out,
        'direction',   CASE WHEN e.tx_is_in THEN 'incoming' WHEN e.tx_is_out THEN 'outgoing' ELSE 'unknown' END,
        'days_between_tx_and_doc',
          CASE
            WHEN e.transaction_date_unix IS NOT NULL AND e.document_date_unix IS NOT NULL
              THEN (ABS(e.transaction_date_unix - e.document_date_unix) / 86400)::int
            ELSE NULL
          END
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

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

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

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

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

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

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

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

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

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

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

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

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

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

create or replace function jsonb_row_diff(old_row jsonb, new_row jsonb) returns jsonb
    language sql
as
$$
  SELECT (
    SELECT jsonb_object_agg(k, jsonb_build_object('old', old_row->k, 'new', new_row->k))
    FROM (
      SELECT DISTINCT key AS k
      FROM (
        SELECT jsonb_object_keys(COALESCE(old_row, '{}'::jsonb)) AS key
        UNION
        SELECT jsonb_object_keys(COALESCE(new_row, '{}'::jsonb)) AS key
      ) AS keys
    ) AS all_keys
    WHERE (old_row->k) IS DISTINCT FROM (new_row->k)
  );
$$;

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

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

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

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

create or replace function post_transaction_to_gl(p_transaction_id uuid) returns void
    language plpgsql
as
$$
DECLARE
    tx RECORD;
    prof RECORD;
    r RECORD;
    entry_id UUID;
BEGIN
    SELECT * INTO tx FROM public.transactions WHERE transaction_id = p_transaction_id;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'Transaction % not found', p_transaction_id;
    END IF;

    -- Loop over all active profiles for this company
    FOR prof IN
        SELECT * FROM public.company_coa_profiles
        WHERE company_id = tx.company_id AND is_active = TRUE
    LOOP
        -- Try to find a matching rule within this profile
        FOR r IN
            SELECT *
            FROM public.gl_rules
            WHERE company_coa_profile_id = prof.company_coa_profile_id
              AND active = TRUE
            ORDER BY priority ASC
        LOOP
            -- Simple condition check (extend as needed)
            IF (r.condition_json ? 'iban' AND tx.iban LIKE (r.condition_json->>'iban'))
               OR (r.condition_json ? 'description' AND tx.description ILIKE ('%' || (r.condition_json->>'description') || '%'))
            THEN
                -- Create journal entry
                INSERT INTO public.journal_entries (
                    company_id, company_coa_profile_id, transaction_id, entry_date, description
                ) VALUES (
                    tx.company_id, prof.company_coa_profile_id, tx.transaction_id, current_date, tx.description
                )
                RETURNING journal_entry_id INTO entry_id;

                -- Debit line
                INSERT INTO public.journal_lines (journal_entry_id, company_coa_profile_id, gl_account_id, debit)
                VALUES (entry_id, prof.company_coa_profile_id, r.debit_gl_account, ABS(COALESCE(tx.debited, 0)));

                -- Credit line
                INSERT INTO public.journal_lines (journal_entry_id, company_coa_profile_id, gl_account_id, credit)
                VALUES (entry_id, prof.company_coa_profile_id, r.credit_gl_account, ABS(COALESCE(tx.credited, 0)));

                -- Log success
                INSERT INTO public.gl_log (company_id, transaction_id, journal_entry_id, action, details)
                VALUES (tx.company_id, tx.transaction_id, entry_id, 'POSTED',
                        jsonb_build_object('rule_id', r.gl_rule_id, 'profile', prof.profile_name));

                EXIT; -- stop after first matching rule
            END IF;
        END LOOP;

        -- If no rules matched for this profile
        IF NOT FOUND THEN
            INSERT INTO public.gl_log (company_id, transaction_id, action, details)
            VALUES (tx.company_id, tx.transaction_id, 'RULE_NOT_FOUND',
                    jsonb_build_object('profile', prof.profile_name, 'transaction', tx));
        END IF;
    END LOOP;
END;
$$;

alter function post_transaction_to_gl(unknown) owner to postgres;

grant execute on function post_transaction_to_gl(unknown) to anon;

grant execute on function post_transaction_to_gl(unknown) to authenticated;

grant execute on function post_transaction_to_gl(unknown) to service_role;

create or replace function post_unposted_transactions(p_company_id uuid) returns void
    language plpgsql
as
$$
DECLARE
    tx RECORD;
BEGIN
    FOR tx IN
        SELECT t.transaction_id
        FROM public.transactions t
        WHERE t.company_id = p_company_id
          AND NOT EXISTS (
              SELECT 1 FROM public.journal_entries je WHERE je.transaction_id = t.transaction_id
          )
    LOOP
        PERFORM public.post_transaction_to_gl(tx.transaction_id);
    END LOOP;
END;
$$;

alter function post_unposted_transactions(unknown) owner to postgres;

grant execute on function post_unposted_transactions(unknown) to anon;

grant execute on function post_unposted_transactions(unknown) to authenticated;

grant execute on function post_unposted_transactions(unknown) to service_role;

create or replace function stargate_get_latest_ponto_token(p_account_id uuid)
    returns TABLE(id bigint, created_at timestamp with time zone, "time" bigint, state text, sub text, access_token text, refresh_token text, account_id uuid, scope text, token_type text, expires_in integer, raw jsonb)
    stable
    language sql
as
$$
  SELECT 
    t.id,
    t.created_at,
    t."time",
    t.state,
    t.sub,
    t.access_token,
    t.refresh_token,
    t.account_id,
    t.scope,
    t.token_type,
    t.expires_in,
    t.raw
  FROM public.stargate_ponto_tokens t
  WHERE t.account_id = p_account_id
  ORDER BY t."time" DESC
  LIMIT 1
$$;

alter function stargate_get_latest_ponto_token(unknown) owner to postgres;

grant execute on function stargate_get_latest_ponto_token(unknown) to anon;

grant execute on function stargate_get_latest_ponto_token(unknown) to authenticated;

grant execute on function stargate_get_latest_ponto_token(unknown) to service_role;

create or replace function stargate_upsert_ponto_accounts(p_accounts jsonb) returns integer
    language plpgsql
as
$$
DECLARE
  v_count integer := 0;
BEGIN
  WITH src AS (
    SELECT a AS item
    FROM jsonb_array_elements(p_accounts) AS a
  ), parsed AS (
    SELECT
      COALESCE(item->'attributes'->>'id', item->>'id') AS ponto_account_id,
      item->'attributes' AS attr,
      item->'relationships' AS rel,
      COALESCE(
        (item->'relationships'->'financialInstitution'->'data'->>'id'),
        (item->'relationships'->'institution'->'data'->>'id')
      ) AS financial_institution_id
    FROM src
  ), enriched AS (
    SELECT
      p.ponto_account_id,
      p.financial_institution_id,
      fi.name AS financial_institution_name,
      fi.logo_url AS financial_institution_avatar,
      -- Resolve company_id from most recent token mapping for this account if present
      s.company_id,
      p.attr
    FROM parsed p
    LEFT JOIN LATERAL (
      SELECT t.state
      FROM public.stargate_ponto_tokens t
      WHERE t.account_id = p.ponto_account_id
      ORDER BY t.time DESC
      LIMIT 1
    ) tok ON TRUE
    LEFT JOIN public.stargate_ponto_states s ON s.state_hashed = tok.state
    LEFT JOIN public.stargate_ponto_financial_institutions fi ON fi.id = p.financial_institution_id
  )
  INSERT INTO public.stargate_ponto_accounts (
    ponto_account_id,
    company_id,
    deprecated,
    description,
    product,
    reference,
    currency,
    subtype,
    available_balance,
    available_balance_changed_at,
    available_balance_reference_date,
    current_balance,
    current_balance_changed_at,
    current_balance_reference_date,
    holder_name,
    reference_type,
    available_balance_variation_observed_at,
    current_balance_variation_observed_at,
    authorization_expiration_expected_at,
    authorized_at,
    internal_reference,
    institution_id,
    financial_institution_id,
    financial_institution_name,
    financial_institution_avatar,
    raw
  )
  SELECT
    e.ponto_account_id,
    e.company_id,
    NULLIF(e.attr->>'deprecated','')::boolean,
    NULLIF(e.attr->>'description',''),
    NULLIF(e.attr->>'product',''),
    NULLIF(e.attr->>'reference',''),
    NULLIF(e.attr->>'currency',''),
    NULLIF(e.attr->>'subtype',''),
    (e.attr->>'availableBalance')::numeric,
    CASE WHEN e.attr ? 'availableBalanceChangedAt' THEN (e.attr->>'availableBalanceChangedAt')::timestamptz ELSE NULL END,
    CASE WHEN e.attr ? 'availableBalanceReferenceDate' THEN (e.attr->>'availableBalanceReferenceDate')::timestamptz ELSE NULL END,
    (e.attr->>'currentBalance')::numeric,
    CASE WHEN e.attr ? 'currentBalanceChangedAt' THEN (e.attr->>'currentBalanceChangedAt')::timestamptz ELSE NULL END,
    CASE WHEN e.attr ? 'currentBalanceReferenceDate' THEN (e.attr->>'currentBalanceReferenceDate')::timestamptz ELSE NULL END,
    NULLIF(e.attr->>'holderName',''),
    NULLIF(e.attr->>'referenceType',''),
    CASE WHEN e.attr ? 'availableBalanceVariationObservedAt' THEN (e.attr->>'availableBalanceVariationObservedAt')::timestamptz ELSE NULL END,
    CASE WHEN e.attr ? 'currentBalanceVariationObservedAt' THEN (e.attr->>'currentBalanceVariationObservedAt')::timestamptz ELSE NULL END,
    CASE WHEN e.attr ? 'authorizationExpirationExpectedAt' THEN (e.attr->>'authorizationExpirationExpectedAt')::timestamptz ELSE NULL END,
    CASE WHEN e.attr ? 'authorizedAt' THEN (e.attr->>'authorizedAt')::timestamptz ELSE NULL END,
    NULLIF(e.attr->>'internalReference',''),
    NULLIF(e.attr->>'institution_id',''),
    e.financial_institution_id,
    e.financial_institution_name,
    e.financial_institution_avatar,
    jsonb_build_object('attributes', e.attr)
  FROM enriched e
  ON CONFLICT (ponto_account_id) DO UPDATE SET
    company_id = EXCLUDED.company_id,
    deprecated = EXCLUDED.deprecated,
    description = EXCLUDED.description,
    product = EXCLUDED.product,
    reference = EXCLUDED.reference,
    currency = EXCLUDED.currency,
    subtype = EXCLUDED.subtype,
    available_balance = EXCLUDED.available_balance,
    available_balance_changed_at = EXCLUDED.available_balance_changed_at,
    available_balance_reference_date = EXCLUDED.available_balance_reference_date,
    current_balance = EXCLUDED.current_balance,
    current_balance_changed_at = EXCLUDED.current_balance_changed_at,
    current_balance_reference_date = EXCLUDED.current_balance_reference_date,
    holder_name = EXCLUDED.holder_name,
    reference_type = EXCLUDED.reference_type,
    available_balance_variation_observed_at = EXCLUDED.available_balance_variation_observed_at,
    current_balance_variation_observed_at = EXCLUDED.current_balance_variation_observed_at,
    authorization_expiration_expected_at = EXCLUDED.authorization_expiration_expected_at,
    authorized_at = EXCLUDED.authorized_at,
    internal_reference = EXCLUDED.internal_reference,
    institution_id = EXCLUDED.institution_id,
    financial_institution_id = EXCLUDED.financial_institution_id,
    financial_institution_name = EXCLUDED.financial_institution_name,
    financial_institution_avatar = EXCLUDED.financial_institution_avatar,
    raw = EXCLUDED.raw;

  GET DIAGNOSTICS v_count = ROW_COUNT;
  RETURN v_count;
END;
$$;

alter function stargate_upsert_ponto_accounts(unknown) owner to postgres;

grant execute on function stargate_upsert_ponto_accounts(unknown) to anon;

grant execute on function stargate_upsert_ponto_accounts(unknown) to authenticated;

grant execute on function stargate_upsert_ponto_accounts(unknown) to service_role;

create or replace function stargate_upsert_ponto_transactions(p_transactions jsonb, p_fallback_account_id text DEFAULT NULL::text) returns integer
    language plpgsql
as
$$
DECLARE
  v_count integer := 0;
BEGIN
  WITH src AS (
    SELECT t AS item FROM jsonb_array_elements(p_transactions) AS t
  ), parsed AS (
    SELECT
      item->>'id' AS ponto_transaction_id,
      item->'attributes' AS attr,
      item->'relationships' AS rel,
      COALESCE((item->'relationships'->'account'->'data'->>'id'), p_fallback_account_id) AS ponto_account_id
    FROM src
  ), enriched AS (
    SELECT
      p.ponto_transaction_id,
      p.ponto_account_id,
      -- Determine company_id from tokens mapping
      s.company_id,
      p.attr
    FROM parsed p
    LEFT JOIN LATERAL (
      SELECT t.state
      FROM public.stargate_ponto_tokens t
      WHERE t.account_id = p.ponto_account_id
      ORDER BY t.time DESC
      LIMIT 1
    ) tok ON TRUE
    LEFT JOIN public.stargate_ponto_states s ON s.state_hashed = tok.state
  )
  INSERT INTO public.stargate_ponto_transactions (
    ponto_transaction_id,
    ponto_account_id,
    company_id,
    amount,
    fee,
    currency,
    description,
    digest,
    additional_information,
    bank_transaction_code,
    card_reference,
    card_reference_type,
    counterpart_name,
    counterpart_reference,
    creditor_id,
    end_to_end_id,
    execution_date,
    mandate_id,
    proprietary_bank_transaction_code,
    purpose_code,
    remittance_information,
    remittance_information_type,
    value_date,
    internal_reference,
    remote_created_at,
    remote_updated_at,
    raw
  )
  SELECT
    e.ponto_transaction_id,
    e.ponto_account_id,
    e.company_id,
    (e.attr->>'amount')::numeric,
    (e.attr->>'fee')::numeric,
    NULLIF(e.attr->>'currency',''),
    NULLIF(e.attr->>'description',''),
    NULLIF(e.attr->>'digest',''),
    NULLIF(e.attr->>'additionalInformation',''),
    NULLIF(e.attr->>'bankTransactionCode',''),
    NULLIF(e.attr->>'cardReference',''),
    NULLIF(e.attr->>'cardReferenceType',''),
    NULLIF(e.attr->>'counterpartName',''),
    NULLIF(e.attr->>'counterpartReference',''),
    NULLIF(e.attr->>'creditorId',''),
    NULLIF(e.attr->>'endToEndId',''),
    CASE WHEN e.attr ? 'executionDate' THEN (e.attr->>'executionDate')::timestamptz ELSE NULL END,
    NULLIF(e.attr->>'mandateId',''),
    NULLIF(e.attr->>'proprietaryBankTransactionCode',''),
    NULLIF(e.attr->>'purposeCode',''),
    NULLIF(e.attr->>'remittanceInformation',''),
    NULLIF(e.attr->>'remittanceInformationType',''),
    CASE WHEN e.attr ? 'valueDate' THEN (e.attr->>'valueDate')::timestamptz ELSE NULL END,
    NULLIF(e.attr->>'internalReference',''),
    CASE WHEN e.attr ? 'createdAt' THEN (e.attr->>'createdAt')::timestamptz ELSE NULL END,
    CASE WHEN e.attr ? 'updatedAt' THEN (e.attr->>'updatedAt')::timestamptz ELSE NULL END,
    jsonb_build_object('attributes', e.attr)
  FROM enriched e
  ON CONFLICT (ponto_transaction_id) DO UPDATE SET
    ponto_account_id = EXCLUDED.ponto_account_id,
    company_id = EXCLUDED.company_id,
    amount = EXCLUDED.amount,
    fee = EXCLUDED.fee,
    currency = EXCLUDED.currency,
    description = EXCLUDED.description,
    digest = EXCLUDED.digest,
    additional_information = EXCLUDED.additional_information,
    bank_transaction_code = EXCLUDED.bank_transaction_code,
    card_reference = EXCLUDED.card_reference,
    card_reference_type = EXCLUDED.card_reference_type,
    counterpart_name = EXCLUDED.counterpart_name,
    counterpart_reference = EXCLUDED.counterpart_reference,
    creditor_id = EXCLUDED.creditor_id,
    end_to_end_id = EXCLUDED.end_to_end_id,
    execution_date = EXCLUDED.execution_date,
    mandate_id = EXCLUDED.mandate_id,
    proprietary_bank_transaction_code = EXCLUDED.proprietary_bank_transaction_code,
    purpose_code = EXCLUDED.purpose_code,
    remittance_information = EXCLUDED.remittance_information,
    remittance_information_type = EXCLUDED.remittance_information_type,
    value_date = EXCLUDED.value_date,
    internal_reference = EXCLUDED.internal_reference,
    remote_created_at = EXCLUDED.remote_created_at,
    remote_updated_at = EXCLUDED.remote_updated_at,
    raw = EXCLUDED.raw;

  GET DIAGNOSTICS v_count = ROW_COUNT;
  RETURN v_count;
END;
$$;

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

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

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

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

create or replace function delete_ponto_user_by_account_id(p_account_id uuid)
    returns TABLE(transactions_deleted bigint, token_usage_deleted bigint, webhooks_deleted bigint, tokens_deleted bigint, accounts_deleted bigint)
    language plpgsql
as
$$
DECLARE
  v_tx bigint := 0;
  v_usage bigint := 0;
  v_webhooks bigint := 0;
  v_tokens bigint := 0;
  v_accounts bigint := 0;
BEGIN
  -- Transactions
  DELETE FROM public.stargate_ponto_transactions WHERE ponto_account_id = p_account_id;
  GET DIAGNOSTICS v_tx = ROW_COUNT;

  -- Token usage logs
  DELETE FROM public.stargate_ponto_token_usage WHERE account_id = p_account_id;
  GET DIAGNOSTICS v_usage = ROW_COUNT;

  -- Webhook events (account_id may be text in some schemas, compare by text)
  DELETE FROM public.stargate_ponto_webhook_events WHERE account_id::text = p_account_id::text;
  GET DIAGNOSTICS v_webhooks = ROW_COUNT;

  -- Tokens that were mapped to this account
  DELETE FROM public.stargate_ponto_tokens WHERE account_id = p_account_id;
  GET DIAGNOSTICS v_tokens = ROW_COUNT;

  -- Account itself
  DELETE FROM public.stargate_ponto_accounts WHERE ponto_account_id = p_account_id;
  GET DIAGNOSTICS v_accounts = ROW_COUNT;

  -- Optional: write an audit log entry
  BEGIN
    INSERT INTO public.stargate_logs (
      time,
      level,
      message,
      meta
    ) VALUES (
      (extract(epoch from now() at time zone 'utc'))::bigint,
      'info',
      'delete_ponto_user_by_account_id executed',
      jsonb_build_object(
        'ponto_account_id', p_account_id,
        'transactions_deleted', v_tx,
        'token_usage_deleted', v_usage,
        'webhooks_deleted', v_webhooks,
        'tokens_deleted', v_tokens,
        'accounts_deleted', v_accounts
      )
    );
  EXCEPTION WHEN others THEN
    -- swallow logging errors
    NULL;
  END;

  RETURN QUERY SELECT v_tx, v_usage, v_webhooks, v_tokens, v_accounts;
END;
$$;

alter function delete_ponto_user_by_account_id(unknown) owner to postgres;

grant execute on function delete_ponto_user_by_account_id(unknown) to anon;

grant execute on function delete_ponto_user_by_account_id(unknown) to authenticated;

grant execute on function delete_ponto_user_by_account_id(unknown) to service_role;

create or replace function import_jortt_customers() returns void
    language plpgsql
as
$$
begin
  insert into public.customers (
    customer_id,
    company_number,
    vat_id,
    note,
    street_address,
    city,
    postal_code,
    country,
    email,
    phone,
    website,
    name,
    company_id,
    status,
    data_provider,
    metadata
  )
  select
    gen_random_uuid(),
    customer_record ->> 'coc_number' as company_number,
    customer_record ->> 'vat_number' as vat_id,
    customer_record ->> 'additional_information' as note,
    customer_record ->> 'finance_address_street' as street_address,
    customer_record ->> 'finance_address_city' as city,
    customer_record ->> 'finance_address_postal_code' as postal_code,
    customer_record ->> 'finance_address_country_code' as country,
    customer_record ->> 'finance_email' as email,
    customer_record ->> 'phonenumber' as phone,
    customer_record ->> 'website' as website,
    customer_record ->> 'finance_company_name' as name,
    company_id,
    'ACTIVE',
    'JORTT',
    jsonb_build_object(
      'jortt_organization_id', customer_record ->> 'organization_id',
      'jortt_aggregate_id', customer_record ->> 'aggregate_id',
      'jortt_ledger_account_id', customer_record ->> 'ledger_account_id',
      'jortt_payment_term', customer_record ->> 'payment_term',
      'jortt_invoice_locale', customer_record ->> 'invoice_language'
    )
  from jortt_invoices ji
  where not exists (
    select 1
    from public.customers c
    where (c.company_number is not null and c.company_number = ji.customer_record ->> 'coc_number')
       or (c.company_number is null and c.email is not null and c.email = ji.customer_record ->> 'finance_email')
  );
end;
$$;

alter function import_jortt_customers() owner to postgres;

grant execute on function import_jortt_customers() to anon;

grant execute on function import_jortt_customers() to authenticated;

grant execute on function import_jortt_customers() to service_role;

create or replace function reset_customers_nl() returns integer
    language plpgsql
as
$$
declare
  upserted_count int;
begin
  with deduped as (
    select distinct on (
      ji.company_id,
      customer_record ->> 'finance_address_country_code',
      customer_record ->> 'coc_number'
    )
      gen_random_uuid() as customer_id,
      customer_record ->> 'coc_number' as company_number,
      case
        when customer_record ->> 'coc_number' is not null
        then ji.company_id || '_' || (customer_record ->> 'finance_address_country_code') || '_' || (customer_record ->> 'coc_number')
        else null
      end as company_country_number,
      customer_record ->> 'vat_number' as vat_id,
      customer_record ->> 'additional_information' as note,
      customer_record ->> 'finance_address_street' as street_address,
      customer_record ->> 'finance_address_city' as city,
      customer_record ->> 'finance_address_postal_code' as postal_code,
      customer_record ->> 'finance_address_country_code' as country,
      customer_record ->> 'finance_email' as email,
      customer_record ->> 'phonenumber' as phone,
      customer_record ->> 'website' as website,
      customer_record ->> 'finance_company_name' as name,
      ji.company_id,
      'ACTIVE' as status,
      'JORTT' as data_provider,
      jsonb_build_object(
        'jortt_organization_id', customer_record ->> 'organization_id',
        'jortt_aggregate_id', customer_record ->> 'aggregate_id',
        'jortt_ledger_account_id', customer_record ->> 'ledger_account_id',
        'jortt_payment_term', customer_record ->> 'payment_term',
        'jortt_invoice_locale', customer_record ->> 'invoice_language'
      ) as metadata
    from jortt_invoices ji
    where (customer_record ->> 'finance_address_country_code') = 'NL'
      and (customer_record ->> 'coc_number') is not null -- 🚫 skip null coc_number rows
    order by ji.company_id,
             customer_record ->> 'finance_address_country_code',
             customer_record ->> 'coc_number',
             ji.id
  )
  insert into public.customers (
    customer_id,
    company_number,
    company_country_number,
    vat_id,
    note,
    street_address,
    city,
    postal_code,
    country,
    email,
    phone,
    website,
    name,
    company_id,
    status,
    data_provider,
    metadata
  )
  select *
  from deduped
  where company_country_number is not null -- 🚫 skip if key is null
  on conflict (company_country_number) do update
  set
    vat_id = excluded.vat_id,
    note = excluded.note,
    street_address = excluded.street_address,
    city = excluded.city,
    postal_code = excluded.postal_code,
    country = excluded.country,
    email = excluded.email,
    phone = excluded.phone,
    website = excluded.website,
    name = excluded.name,
    company_id = excluded.company_id,
    status = excluded.status,
    data_provider = excluded.data_provider,
    metadata = excluded.metadata;

  get diagnostics upserted_count = row_count;
  return upserted_count;
end;
$$;

alter function reset_customers_nl() owner to postgres;

grant execute on function reset_customers_nl() to anon;

grant execute on function reset_customers_nl() to authenticated;

grant execute on function reset_customers_nl() to service_role;

create or replace function post_transaction(p_transaction_id uuid) returns void
    language plpgsql
as
$$
declare
    tx record;
    prof record;
    r record;
    entry_id uuid;
begin
    select * into tx from public.transactions where transaction_id = p_transaction_id;
    if not found then
        raise exception 'Transaction % not found', p_transaction_id;
    end if;

    -- Loop over all active profiles for this company
    for prof in
        select * from public.company_coa_profiles
        where company_id = tx.company_id and is_active = true
    loop
        -- Try to find a matching rule within this profile
        for r in
            select *
            from public.gl_rules
            where company_coa_profile_id = prof.company_coa_profile_id
              and active = true
            order by priority asc
        loop
            if (r.condition_json ? 'iban' and tx.iban like (r.condition_json->>'iban'))
               or (r.condition_json ? 'description' and tx.description ilike ('%' || (r.condition_json->>'description') || '%'))
            then
                -- Create journal entry
                insert into public.journal_entries (
                    company_id, company_coa_profile_id, transaction_id, entry_date, description
                ) values (
                    tx.company_id, prof.company_coa_profile_id, tx.transaction_id, current_date, tx.description
                )
                returning journal_entry_id into entry_id;

                -- Debit line
                insert into public.journal_lines (journal_entry_id, company_coa_profile_id, company_gl_account_id, debit)
                values (entry_id, prof.company_coa_profile_id, r.debit_company_gl_account, abs(coalesce(tx.debited, 0)));

                -- Credit line
                insert into public.journal_lines (journal_entry_id, company_coa_profile_id, company_gl_account_id, credit)
                values (entry_id, prof.company_coa_profile_id, r.credit_company_gl_account, abs(coalesce(tx.credited, 0)));

                -- Log success
                insert into public.gl_log (company_id, transaction_id, journal_entry_id, action, details)
                values (tx.company_id, tx.transaction_id, entry_id, 'POSTED',
                        jsonb_build_object('rule_id', r.gl_rule_id, 'profile', prof.profile_name));

                exit; -- stop after first matching rule
            end if;
        end loop;

        -- If no rules matched for this profile
        if not found then
            insert into public.gl_log (company_id, transaction_id, action, details)
            values (tx.company_id, tx.transaction_id, 'RULE_NOT_FOUND',
                    jsonb_build_object('profile', prof.profile_name, 'transaction', tx));
        end if;
    end loop;
end;
$$;

alter function post_transaction(unknown) owner to postgres;

grant execute on function post_transaction(unknown) to anon;

grant execute on function post_transaction(unknown) to authenticated;

grant execute on function post_transaction(unknown) to service_role;

create or replace function apply_gl_rule(p_transaction_id uuid, p_rule_id uuid) returns void
    language plpgsql
as
$$
declare
    tx record;
    r record;
    entry_id uuid;
    vat_rate numeric := 0;
    base_amount numeric;
    vat_amount numeric;
begin
    select * into tx from public.transactions where transaction_id = p_transaction_id;
    select * into r from public.gl_rules where gl_rule_id = p_rule_id;

    if r.vat_code is not null then
        select rate into vat_rate from public.vat_codes where vat_code = r.vat_code;
    end if;

    -- Split amounts
    base_amount := abs(coalesce(tx.amount, 0));
    vat_amount := round(base_amount * vat_rate, 2);

    -- Create journal entry
    insert into public.journal_entries (company_id, company_coa_profile_id, transaction_id, entry_date, description)
    values (tx.company_id, r.company_coa_profile_id, tx.transaction_id, current_date, tx.description)
    returning journal_entry_id into entry_id;

    -- Debit side
    insert into public.journal_lines (journal_entry_id, company_coa_profile_id, company_gl_account_id, debit)
    values (entry_id, r.debit_company_gl_account, base_amount + vat_amount);

    -- Credit base revenue/costs
    insert into public.journal_lines (journal_entry_id, company_coa_profile_id, company_gl_account_id, credit)
    values (entry_id, r.credit_company_gl_account, base_amount);

    -- Credit VAT payable or Debit VAT receivable
    if vat_amount > 0 then
        if tx.amount > 0 then -- sale
            insert into public.journal_lines (journal_entry_id, company_coa_profile_id, company_gl_account_id, credit)
            values (entry_id, r.vat_account_credit, vat_amount);
        else -- purchase
            insert into public.journal_lines (journal_entry_id, company_coa_profile_id, company_gl_account_id, debit)
            values (entry_id, r.vat_account_debit, vat_amount);
        end if;
    end if;
end;
$$;

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

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

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

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

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

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

  -- 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 := 90;  -- widened grace

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

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

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

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

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

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

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

  -- matching mode
  v_directional_matching boolean := true;

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

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

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

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

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

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

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

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

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

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

  transactions_scope as (
    select
      t.transaction_id,
      t.company_id,                       -- text
      t.amount,
      t.debited,
      t.credited,
      t.currency,

      -- Broader extractor: matches "FACTUUR 202601-3" etc.
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.invoice_nr,''),
            (select m[1]
             from regexp_matches(
               upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
               '(INV(?:OICE)?[ -_/]?[A-Z0-9._-]{1,24}|FACTUUR[ -_/]?[A-Z0-9._-]{1,24}|[0-9]{5,10}[-/][A-Z0-9]{1,10})',
               'g'
             ) as m
             limit 1)
          )
        ),
      '') as invoice_nr_norm,

      -- Normalized blob for containment checks
      public.norm_code(upper(concat_ws(' ',
        t.remittance_information, t.description, t.transfer_reference, t.processed_description
      ))) as tx_text_code,

      t.completed_date,

      (nullif(t.credited,0) is not null or nullif(t.money_in,0)  is not null)  as tx_is_in,
      (nullif(t.debited,0)  is not null or nullif(t.money_out,0) is not null)  as tx_is_out,

      nullif(public.norm_text_ns(
        coalesce(
          t.name_consumer,
          t.account_holder_name,
          t.account_holder_name_counterparty
        )
      ), '') as tx_customer_ns,

      coalesce(
        t.name_consumer,
        t.account_holder_name,
        t.account_holder_name_counterparty
      ) as tx_customer_display,

      case
        when t.debited  is not null and t.debited  > 0 then t.debited
        when t.credited is not null and t.credited > 0 then t.credited
        when t.amount   is not null then abs(t.amount)
        else null
      end as effective_amount
    from public.transactions t
    where (p_company_id is null or t.company_id = p_company_id::text)
      and t.reconciled is not true
      and (
           t.invoice_nr is not null
        or (t.currency is not null and (t.name_consumer is not null or t.account_holder_name is not null or t.account_holder_name_counterparty is not null))
        or (t.currency is not null and (t.amount is not null or t.debited is not null or t.credited is not null))
      )
    order by t.completed_date desc, t.transaction_id
    limit v_max_transactions_to_scan
  ),

  /* ================== seeds (available signals only) ================== */

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

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

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

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

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

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

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

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

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

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

  attributes as (
    select
      p.transaction_id,
      p.document_id,
      d.company_id_uuid       as document_company_id,

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

      t.invoice_nr_norm     as tx_invoice_norm,
      d.invoice_number_norm as doc_invoice_norm,

      d.invoice_number_raw  as doc_invoice_raw,

      t.completed_date      as transaction_date_unix,
      d.document_date_unix  as document_date_unix,

      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_customer_display  as transaction_customer_name,
      d.doc_customer_display as document_customer_name,

      t.tx_text_code,

      coalesce(nullif(d.doc_dedup_key,''), d.document_id::text) as doc_dedup_key,

      t.effective_amount as transaction_effective_amount,

      t.tx_is_in,
      t.tx_is_out
    from distinct_pairs p
    join transactions_scope t on t.transaction_id = p.transaction_id
    join invoice_scope    d on d.document_id    = p.document_id
    where t.company_id = d.company_id_text
  ),

  evaluated as (
    select
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,

      coalesce((
        (a.tx_invoice_norm is not null and a.doc_invoice_norm is not null and a.tx_invoice_norm = a.doc_invoice_norm)
        or
        (a.tx_text_code is not null and a.doc_invoice_norm is not null and position(a.doc_invoice_norm in a.tx_text_code) > 0)
      ), false) as invoice_number_match,

      coalesce(a.transaction_currency  = a.document_currency, false) as currency_match,

      ( a.tx_customer_ns is not null and a.doc_customer_ns is not null and
        ( a.tx_customer_ns = a.doc_customer_ns
          or position(a.doc_customer_ns in a.tx_customer_ns) > 0
          or position(a.tx_customer_ns in a.doc_customer_ns) > 0)
      ) as customer_match,

      coalesce(abs(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, false) as date_within_cap,

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

      (a.transaction_debited  is not null and a.transaction_debited  > 0 and a.document_total = a.transaction_debited)  as amount_exact_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and a.document_total = a.transaction_credited) as amount_exact_from_credited,
      (a.transaction_debited  is not null and a.transaction_debited  > 0 and abs(a.document_total - a.transaction_debited)  <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and abs(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_credited,

      a.tx_is_in,
      a.tx_is_out,

      -- defensive flags for summary
      (a.transaction_credited  is not null and a.transaction_credited  > 0) as credited_pos,
      (a.transaction_debited   is not null and a.transaction_debited   > 0) as debited_pos
    from attributes a
  ),

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

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

  scored_components as (
    select
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,

      m.transaction_date_unix,
      m.document_date_unix,

      m.tx_is_in, m.tx_is_out,

      coalesce(
        case
          when m.amount_exact_match then w.amount_weight
          when m.amount_tolerance_match then
            w.amount_weight * exp(
              -0.5 * power(
                (abs(m.transaction_effective_amount - m.document_total))
                / nullif((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0)
              , 2)
            )
          else 0
        end, 0
      )::numeric as amount_score,

      (m.amount_exact_match or m.amount_tolerance_match) as amount_match,
      m.invoice_number_match  as invoice_number_match,

      case
        when m.transaction_date_unix is not null and m.document_date_unix is not null then
          w.date_weight * exp(
            -0.5 * power(
              greatest(0,
                (abs(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days
              ) / nullif( greatest(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k ), 0)
            , 2)
          )
        else 0
      end::numeric as date_score,

      m.date_within_cap as date_match,

      m.currency_match  as currency_match,
      m.customer_match  as customer_match,

      w.amount_weight   as amount_weight,
      w.invoice_weight  as invoice_weight,
      w.date_weight     as date_weight,
      w.currency_weight as currency_weight,
      w.customer_weight as customer_weight,

      (coalesce(m.invoice_number_match::int,0)  * w.invoice_weight)::numeric  as invoice_score,
      (coalesce(m.currency_match::int,0)        * w.currency_weight)::numeric as currency_score,
      (coalesce(m.customer_match::int,0)        * w.customer_weight)::numeric as customer_score,

      m.transaction_effective_amount,
      m.document_total
    from with_amount_tolerance m
    cross join resolved_weights w
  ),

  scored_with_confidence as (
    select
      s.*,

      (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight)::numeric as dynamic_total_weight,

      (s.amount_score + s.invoice_score + s.date_score + s.currency_score + s.customer_score)::numeric as total_numerator,

      (coalesce(s.amount_match::int,0)
     + coalesce(s.invoice_number_match::int,0)
     + coalesce(s.date_match::int,0)
     + coalesce(s.currency_match::int,0)
     + coalesce(s.customer_match::int,0)) as signals_matched_count,

      case when
        (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight) > 0
      then
        round(((s.amount_score + s.invoice_score + s.date_score + s.currency_score + s.customer_score)
              / (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight)) * 100.0, 4)
      else 0 end as confidence_percent
    from scored_components s
  ),

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

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

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

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

  allowed_for_insert as (
    select f.*
    from   final_candidates f
    where  not exists (select 1 from public.reconciliations r1 where r1.transaction_id = f.transaction_id)
       and not exists (select 1 from public.reconciliations r2 where r2.document_id    = f.document_id)
  ),
  inserted_reconciliations as (
    insert into public.reconciliations (
      transaction_id, document_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary, note
    )
    with ts_base as (
      select f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             coalesce(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint as base_unix
      from allowed_for_insert f
    ),
    enrich as (
      select
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match as invoice_match,
        d.amount_match, d.date_match, d.currency_match, d.customer_match,

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

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

        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw,

        -- direction flags (+ defensive)
        (a.tx_is_in  or e.credited_pos) as tx_is_in_final,
        (a.tx_is_out or e.debited_pos ) as tx_is_out_final,

        e.credited_pos, e.debited_pos
      from ts_base t
      join final_candidates d on d.transaction_id = t.transaction_id and d.document_id = t.document_id
      join evaluated        e on e.transaction_id = d.transaction_id and e.document_id = d.document_id
      join attributes       a on a.transaction_id = d.transaction_id and a.document_id = d.document_id
    ),

    notes as (
      select
        e.*,
        case
          when e.document_total is not null and e.document_total <> 0
          then round(abs(e.transaction_effective_amount - e.document_total) / nullif(e.document_total,0) * 100.0, 2)
          else null
        end as amt_diff_pct,

        case
          when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix)
          else null
        end as sec_diff,

        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 86400.0
          else null end as day_gap,

        case when e.amount_weight > 0 then round((e.amount_score / e.amount_weight) * 100.0, 2) end as amount_ratio_pct,
        case when e.date_weight   > 0 then round((e.date_score   / e.date_weight)   * 100.0, 0)  end as date_ratio_pct
      from enrich e
    ),

    notes_text as (
      select
        n.*,
        case
          when n.date_weight > 0 and n.date_ratio_pct <= 99 then
            trim(both ' ' from concat_ws(' ',
              case
                when n.amount_weight > 0 and n.amount_score < n.amount_weight and n.amt_diff_pct is not null
                then concat('Amount scored ', coalesce(n.amount_ratio_pct::text,'0'),
                            '% because payment vs invoice differed by ', n.amt_diff_pct::text, '%.')
              end,
              case
                when n.day_gap is not null
                then concat('Date scored ', coalesce(n.date_ratio_pct::text,'0'),
                            '% because dates were ', round(n.day_gap::numeric, 2)::text, ' days apart.')
              end
            ))
          else null
        end as note_text
      from notes n
    )
    select
      e.transaction_id, e.document_id, e.document_company_id,
      coalesce(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      extract(year from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      extract(quarter from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) at time zone 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          select json_agg(s) from unnest(array[
            case when e.invoice_match  then 'invoice'  end,
            case when e.amount_match   then 'amount'   end,
            case when e.date_match     then 'date'     end,
            case when e.currency_match then 'currency' end,
            case when e.customer_match then 'customer' end
          ]) s where s is not null
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  coalesce(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_is_in',     e.tx_is_in_final,
        'tx_is_out',    e.tx_is_out_final,
        'direction',    case when e.tx_is_in_final then 'incoming'
                             when e.tx_is_out_final then 'outgoing'
                             else 'unknown' end,
        'days_between_tx_and_doc',
          case
            when e.transaction_date_unix is not null and e.document_date_unix is not null
              then (abs(e.transaction_date_unix - e.document_date_unix) / 86400)::int
            else null
          end
      )::text,
      nullif(nt.note_text,'')
    from notes_text nt
    join enrich e using (transaction_id, document_id)
    returning reconciliation_id, transaction_id, 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,
      false as order_match,
      false as merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      0 as order_weight,
      0 as merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      0 as order_score,
      0 as merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    from inserted_reconciliations r
    join scored_with_confidence d
      on d.transaction_id = r.transaction_id
     and d.document_id    = r.document_id
    returning 1
  ),

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

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

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

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

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

  get stacked diagnostics
    v_err_state   = returned_sqlstate,
    v_err_msg     = message_text,
    v_err_detail  = pg_exception_detail,
    v_err_hint    = pg_exception_hint,
    v_err_context = pg_exception_context;

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

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

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

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

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

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

create or replace function athena_reconcile_experimental(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.10A-EXP';
v_start_time timestamptz := clock_timestamp();
v_elapsed_ms bigint := 0;
-- config
v_config_amount_tolerance numeric;
v_config_date_window_days numeric;
v_config_min_confidence numeric;
v_config_allow_document_duplicates boolean;
v_config_log_activity boolean;
-- derived
v_amount_tolerance_fraction numeric;
v_date_window_seconds bigint;
v_date_full_weight_grace_days int := 30;
-- Gaussian params
v_edge_weight_small constant numeric := 0.10;
-- score at the boundary (10%)
v_gauss_k constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small));
-- ~2.146
v_cap_precise int := 5;
v_cap_broad int := 10;
v_cap_window_only int := 3;
v_cap_iban_exact int := 5;
v_iban_exact_window_seconds bigint;
v_max_transactions_to_scan int := 20000;
-- weights
v_weights_json jsonb := '{}'::jsonb;
v_weights_total_possible numeric := 0;
-- counts
v_summary_top_per_tx bigint := 0;
v_summary_top_per_doc bigint := 0;
v_summary_passed_count bigint := 0;
v_inserted_count bigint := 0;
v_candidates_count bigint := 0;
-- input sizes
v_total_tx bigint := 0;
v_total_docs bigint := 0;
-- per-seed stats
v_seed_counts_total jsonb := '{}'::jsonb;
v_seed_counts_distinct jsonb := '{}'::jsonb;
-- run meta
v_log_run_id uuid := gen_random_uuid();
v_log_run_unix_time bigint := EXTRACT(
  EPOCH
  FROM now()
)::bigint;
-- matching mode
v_directional_matching boolean := TRUE;
-- errors
v_err_state text;
v_err_msg text;
v_err_detail text;
v_err_hint text;
v_err_context text;
BEGIN -- config
SELECT amount_tolerance,
  date_window_days,
  min_confidence,
  allow_doc_duplicates,
  log_activity INTO v_config_amount_tolerance,
  v_config_date_window_days,
  v_config_min_confidence,
  v_config_allow_document_duplicates,
  v_config_log_activity
FROM public.reconciliations_config
WHERE reconciliation_config_id = p_reconciliation_config_id;
IF NOT FOUND THEN v_elapsed_ms := FLOOR(
  EXTRACT(
    EPOCH
    FROM (clock_timestamp() - v_start_time)
  ) * 1000
)::bigint;
RETURN jsonb_build_object(
  'msg',
  'reconciliations_config not found',
  'time_ms',
  v_elapsed_ms,
  'transactions_scanned',
  0,
  'documents_considered',
  0,
  'top_per_tx',
  0,
  'top_per_doc',
  0,
  'passed_confidence',
  0
);
END IF;
-- tolerances/windows
v_amount_tolerance_fraction := CASE
  WHEN v_config_amount_tolerance >= 1 THEN v_config_amount_tolerance / 100.0
  ELSE v_config_amount_tolerance
END;
v_date_window_seconds := CEIL(COALESCE(v_config_date_window_days, 1) * 86400)::bigint;
v_iban_exact_window_seconds := LEAST(v_date_window_seconds, 14 * 86400)::bigint;
IF v_config_min_confidence IS NULL THEN v_config_min_confidence := 0;
END IF;
IF v_config_min_confidence <= 1 THEN v_config_min_confidence := v_config_min_confidence * 100.0;
END IF;
-- weights
WITH candidate_weights AS (
  SELECT LOWER(TRIM(w.weight_key)) AS k_norm,
    (w.weight_value)::numeric AS v_norm,
    w.company_id
  FROM public.reconciliations_weight_registry w
  WHERE w.active
    AND w.merchant_id IS NULL
    AND (
      (
        p_company_id IS NULL
        AND w.company_id IS NULL
      )
      OR (
        p_company_id IS NOT NULL
        AND (
          w.company_id = p_company_id
          OR w.company_id IS NULL
        )
      )
    )
    AND LOWER(TRIM(w.weight_key)) IN (
      'amount_total',
      'invoice_number',
      'order_number',
      'date',
      'currency',
      'merchant_id',
      'customer_name'
    )
),
picked AS (
  SELECT DISTINCT ON (k_norm) k_norm,
    v_norm
  FROM candidate_weights
  ORDER BY k_norm,
    (company_id IS NULL)
)
SELECT COALESCE(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) INTO v_weights_json
FROM picked;
SELECT COALESCE((v_weights_json->>'amount_total')::numeric, 0) + COALESCE((v_weights_json->>'invoice_number')::numeric, 0) + COALESCE((v_weights_json->>'order_number')::numeric, 0) + COALESCE((v_weights_json->>'date')::numeric, 0) + COALESCE((v_weights_json->>'currency')::numeric, 0) + COALESCE((v_weights_json->>'merchant_id')::numeric, 0) + COALESCE((v_weights_json->>'customer_name')::numeric, 0) INTO v_weights_total_possible;
IF v_weights_total_possible = 0 THEN v_weights_json := jsonb_build_object(
  'amount_total',
  1,
  'invoice_number',
  1,
  'order_number',
  1,
  'date',
  1,
  'currency',
  1,
  'merchant_id',
  1,
  'customer_name',
  1
);
v_weights_total_possible := 7;
END IF;
-- run_start log
INSERT INTO public.reconciliations_logs (
    run_id,
    function_version,
    action,
    target,
    status,
    message,
    company_id,
    transaction_id,
    document_id,
    reconciliation_id,
    rows_affected,
    confidence,
    step_ms,
    processing_ms,
    context
  )
VALUES (
    v_log_run_id,
    v_engine_version,
    'run_start',
    'function',
    'ok',
    NULL,
    p_company_id,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    jsonb_build_object(
      'args',
      jsonb_build_object(
        'log_activity',
        v_config_log_activity,
        'allow_doc_dups',
        v_config_allow_document_duplicates,
        'amount_tol_pct',
        v_amount_tolerance_fraction,
        'min_confidence',
        v_config_min_confidence,
        'date_window_days',
        v_config_date_window_days,
        'max_tx_seed',
        v_max_transactions_to_scan,
        'cap_precise',
        v_cap_precise,
        'cap_broad',
        v_cap_broad,
        'cap_window_only',
        v_cap_window_only,
        'cap_iban_exact',
        v_cap_iban_exact,
        'iban_exact_window_days',
        ROUND(v_iban_exact_window_seconds / 86400.0, 2),
        'directional_matching',
        v_directional_matching,
        'gauss_edge_weight',
        v_edge_weight_small
      ),
      'weights_json',
      v_weights_json,
      'weights_max_possible',
      v_weights_total_possible
    )
  );
-- ====================== pipeline ======================
WITH document_scope AS (
  -- documents_v2 branch
  SELECT d.document_id,
    d.company_id,
    d.total,
    d.currency,
    NULLIF(d.invoice_number, '') AS invoice_number_raw,
    NULLIF(d.order_id, '') AS order_id_raw,
    NULLIF(public.norm_code(d.invoice_number), '') AS invoice_number_norm,
    NULLIF(public.norm_code(d.order_id), '') AS order_id_norm,
    CASE
      WHEN d.document_date_unix IS NOT NULL THEN d.document_date_unix
      WHEN d.document_date ~ '^\d{4}-[A-Za-z]+-\d{1,2}$' THEN EXTRACT(
        EPOCH
        FROM to_timestamp(d.document_date, 'YYYY-Month-FMDD')
      )::bigint
      WHEN d.document_date ~ '^\d{1,2}-[A-Za-z]+-\d{4}$' THEN EXTRACT(
        EPOCH
        FROM to_timestamp(d.document_date, 'FMDD-Month-YYYY')
      )::bigint
      WHEN d.document_date ~ '^\d{4}-\d{1,2}-\d{1,2}$' THEN EXTRACT(
        EPOCH
        FROM to_timestamp(d.document_date, 'YYYY-FMMM-FMDD')
      )::bigint
      WHEN d.document_date ~ '^\d{1,2}-\d{1,2}-\d{4}$' THEN EXTRACT(
        EPOCH
        FROM to_timestamp(d.document_date, 'FMDD-FMMM-YYYY')
      )::bigint
      ELSE NULL
    END AS document_date_unix,
    d.merchant_id,
    d.customer_name,
    NULLIF(
      public.norm_core_ns(
        COALESCE(d.merchant_name, d.merchant_info::jsonb->>'name')
      ),
      ''
    ) AS doc_merchant_ns,
    NULLIF(
      public.norm_text_ns(
        concat_ws(
          ' ',
          d.customer_name,
          d.customer_info::jsonb->>'name',
          d.customer_info::jsonb->>'contact_person',
          d.customer_info::jsonb->>'company_name',
          d.customer_info::jsonb->>'email'
        )
      ),
      ''
    ) AS doc_customer_ns,
    NULLIF(
      public.norm_iban(d.merchant_info::jsonb->>'iban'),
      ''
    ) AS doc_iban_ns,
    NULLIF(
      public.norm_iban_core(d.merchant_info::jsonb->>'iban'),
      ''
    ) AS doc_iban_core_ns,
    COALESCE(
      d.customer_name,
      d.customer_info::jsonb->>'name',
      d.customer_info::jsonb->>'contact_person',
      d.customer_info::jsonb->>'company_name',
      d.customer_info::jsonb->>'email'
    ) AS doc_customer_display,
    COALESCE(
      NULLIF(d.dedup_serialized, ''),
      d.document_id::text
    ) AS doc_dedup_key
  FROM public.documents_v2 d
  WHERE (
      p_company_id IS NULL
      OR d.company_id = p_company_id
    )
    AND d.reconciled IS NOT TRUE
  UNION ALL
  -- invoices branch
  SELECT i.invoice_id AS document_id,
    i.recipient_company_id AS company_id,
    i.total,
    i.currency,
    NULLIF(i.invoice_nr, '') AS invoice_number_raw,
    NULL AS order_id_raw,
    NULLIF(public.norm_code(i.invoice_nr), '') AS invoice_number_norm,
    NULL AS order_id_norm,
    CASE
      WHEN i.issue_date IS NOT NULL THEN i.issue_date::bigint
      WHEN i.due_date IS NOT NULL THEN i.due_date::bigint
      ELSE NULL
    END AS document_date_unix,
    NULL::uuid AS merchant_id,
    i.recipient_name AS customer_name,
    NULLIF(public.norm_core_ns(i.author_name), '') AS doc_merchant_ns,
    NULLIF(
      public.norm_text_ns(
        concat_ws(
          ' ',
          i.recipient_name,
          i.recipient_first_name,
          i.recipient_last_name,
          i.recipient_email,
          i.recipient_company
        )
      ),
      ''
    ) AS doc_customer_ns,
    NULL AS doc_iban_ns,
    NULL AS doc_iban_core_ns,
    COALESCE(
      i.recipient_name,
      i.recipient_company,
      i.recipient_email
    ) AS doc_customer_display,
    i.invoice_id::text AS doc_dedup_key
  FROM public.invoices i
  WHERE (
      p_company_id IS NULL
      OR i.recipient_company_id = p_company_id
    )
    AND i.reconciliation_id IS NULL
),
transactions_scope AS (
  SELECT t.transaction_id,
    t.company_id,
    t.amount,
    t.debited,
    t.credited,
    t.currency,
    NULLIF(
      public.norm_code(
        COALESCE(
          NULLIF(t.invoice_nr, ''),
          (
            SELECT m [1]
            FROM regexp_matches(
                UPPER(
                  concat_ws(
                    ' ',
                    t.remittance_information,
                    t.description,
                    t.transfer_reference,
                    t.processed_description
                  )
                ),
                '(INV(?:OICE)?[ -_/]?\d{1,20}|[A-Z]{1,6}[-/]\d{2,20}|[0-9]{4}[-/][0-9]{2,20})',
                'g'
              ) AS m
            LIMIT 1
          )
        )
      ), ''
    ) AS invoice_nr_norm,
    NULLIF(
      public.norm_code(
        COALESCE(
          NULLIF(t.order_id, ''),
          (
            SELECT m [1]
            FROM regexp_matches(
                UPPER(
                  concat_ws(
                    ' ',
                    t.remittance_information,
                    t.description,
                    t.transfer_reference,
                    t.processed_description
                  )
                ),
                '(ORD(?:ER)?[ -_/]?\d{2,20}|PO[ -_/]?\d{2,20}|ORDER[ -_/]?\d{2,20})',
                'g'
              ) AS m
            LIMIT 1
          )
        )
      ), ''
    ) AS order_id_norm,
    t.completed_date,
    t.merchant_id,
    t.name_consumer,
    (
      NULLIF(t.credited, 0) IS NOT NULL
      OR NULLIF(t.money_in, 0) IS NOT NULL
    ) AS tx_is_in,
    (
      NULLIF(t.debited, 0) IS NOT NULL
      OR NULLIF(t.money_out, 0) IS NOT NULL
    ) AS tx_is_out,
    NULLIF(
      public.norm_core_ns(
        COALESCE(
          t.name_beneficiary,
          t.account_holder_name_counterparty,
          t.bank_counterparty
        )
      ),
      ''
    ) AS tx_merchant_ns,
    NULLIF(
      public.norm_text_ns(
        COALESCE(
          t.name_consumer,
          t.account_holder_name,
          t.account_holder_name_counterparty
        )
      ),
      ''
    ) AS tx_customer_ns,
    NULLIF(
      public.norm_iban(COALESCE(t.iban_beneficiary, t.iban, '')),
      ''
    ) AS tx_iban_beneficiary_ns,
    NULLIF(
      public.norm_iban(COALESCE(t.iban_counterparty, '')),
      ''
    ) AS tx_iban_counterparty_ns,
    NULLIF(
      public.norm_iban_core(COALESCE(t.iban_beneficiary, t.iban, '')),
      ''
    ) AS tx_iban_beneficiary_core_ns,
    NULLIF(
      public.norm_iban_core(COALESCE(t.iban_counterparty, '')),
      ''
    ) AS tx_iban_counterparty_core_ns,
    COALESCE(
      t.name_consumer,
      t.account_holder_name,
      t.account_holder_name_counterparty
    ) AS tx_customer_display,
    CASE
      WHEN t.debited IS NOT NULL
      AND t.debited > 0 THEN t.debited
      WHEN t.credited IS NOT NULL
      AND t.credited > 0 THEN t.credited
      WHEN t.amount IS NOT NULL THEN ABS(t.amount)
      ELSE NULL
    END AS effective_amount
  FROM public.transactions t
  WHERE (
      p_company_id IS NULL
      OR t.company_id = p_company_id::text
    )
    AND t.reconciled IS NOT TRUE
    AND (
      t.invoice_nr IS NOT NULL
      OR t.order_id IS NOT NULL
      OR (
        t.currency IS NOT NULL
        AND t.merchant_id IS NOT NULL
      )
      OR (
        t.currency IS NOT NULL
        AND (
          t.name_consumer IS NOT NULL
          OR t.account_holder_name IS NOT NULL
          OR t.account_holder_name_counterparty IS NOT NULL
        )
      )
      OR (
        t.currency IS NOT NULL
        AND (
          t.amount IS NOT NULL
          OR t.debited IS NOT NULL
          OR t.credited IS NOT NULL
        )
      )
    )
  ORDER BY t.completed_date DESC,
    t.transaction_id
  LIMIT v_max_transactions_to_scan
),
/* ================== staged seeding (specific → broad) ================== */
s1_invoice AS (
  SELECT t.transaction_id,
    d.document_id,
    'invoice_number' AS seed
  FROM transactions_scope t
    JOIN document_scope d ON d.company_id = t.company_id::uuid
    AND d.invoice_number_norm IS NOT NULL
    AND t.invoice_nr_norm IS NOT NULL
    AND d.invoice_number_norm = t.invoice_nr_norm
    AND d.currency = t.currency
),
s2_order AS (
  SELECT s.*
  FROM (
      SELECT t.transaction_id,
        d.document_id,
        'order_number' AS seed
      FROM transactions_scope t
        JOIN document_scope d ON d.company_id = t.company_id::uuid
        AND d.order_id_norm IS NOT NULL
        AND t.order_id_norm IS NOT NULL
        AND d.order_id_norm = t.order_id_norm
        AND d.currency = t.currency
    ) s
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
    ) u USING (transaction_id)
  WHERE u.transaction_id IS NULL
),
-- IBAN exact amount (no date window)
s3_iban_exact_amount_out AS (
  SELECT t.transaction_id,
    d_sub.document_id,
    'iban_exact_amount_out' AS seed
  FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          (NOT v_directional_matching)
          OR t.tx_is_out
        )
        AND d.currency = t.currency
        AND t.debited IS NOT NULL
        AND t.debited > 0
        AND d.total = t.debited
        AND (
          (
            d.doc_iban_ns IS NOT NULL
            AND t.tx_iban_counterparty_ns IS NOT NULL
            AND d.doc_iban_ns = t.tx_iban_counterparty_ns
          )
          OR (
            d.doc_iban_core_ns IS NOT NULL
            AND t.tx_iban_counterparty_core_ns IS NOT NULL
            AND d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns
          )
        )
      ORDER BY d.document_id
      LIMIT v_cap_iban_exact
    ) d_sub ON TRUE
  WHERE done.transaction_id IS NULL
),
s3_iban_exact_amount_in AS (
  SELECT t.transaction_id,
    d_sub.document_id,
    'iban_exact_amount_in' AS seed
  FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          (NOT v_directional_matching)
          OR t.tx_is_in
        )
        AND d.currency = t.currency
        AND t.credited IS NOT NULL
        AND t.credited > 0
        AND d.total = t.credited
        AND (
          (
            d.doc_iban_ns IS NOT NULL
            AND t.tx_iban_beneficiary_ns IS NOT NULL
            AND d.doc_iban_ns = t.tx_iban_beneficiary_ns
          )
          OR (
            d.doc_iban_core_ns IS NOT NULL
            AND t.tx_iban_beneficiary_core_ns IS NOT NULL
            AND d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns
          )
        )
      ORDER BY d.document_id
      LIMIT v_cap_iban_exact
    ) d_sub ON TRUE
  WHERE done.transaction_id IS NULL
),
s3_iban_exact_amount AS (
  SELECT *
  FROM s3_iban_exact_amount_out
  UNION ALL
  SELECT *
  FROM s3_iban_exact_amount_in
),
s4_merchant_id_exact_amount AS (
  SELECT s.*
  FROM (
      SELECT t.transaction_id,
        d.document_id,
        'merchant_id_exact_amount' AS seed
      FROM transactions_scope t
        JOIN document_scope d ON d.company_id = t.company_id::uuid
        AND (
          (NOT v_directional_matching)
          OR t.tx_is_out
        )
        AND d.currency = t.currency
        AND d.merchant_id = t.merchant_id
        AND t.debited IS NOT NULL
        AND t.debited > 0
        AND d.total = t.debited
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
    ) s
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
    ) done USING (transaction_id)
  WHERE done.transaction_id IS NULL
),
s5_merchant_ns_exact_amount AS (
  SELECT s.*
  FROM (
      SELECT t.transaction_id,
        d.document_id,
        'merchant_ns_exact_amount' AS seed
      FROM transactions_scope t
        JOIN document_scope d ON d.company_id = t.company_id::uuid
        AND (
          (NOT v_directional_matching)
          OR t.tx_is_out
        )
        AND d.currency = t.currency
        AND d.doc_merchant_ns IS NOT NULL
        AND t.tx_merchant_ns IS NOT NULL
        AND (
          d.doc_merchant_ns = t.tx_merchant_ns
          OR position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
          OR position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
        )
        AND t.debited IS NOT NULL
        AND t.debited > 0
        AND d.total = t.debited
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
    ) s
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s4_merchant_id_exact_amount
    ) done USING (transaction_id)
  WHERE done.transaction_id IS NULL
),
s6_customer_ns_exact_amount AS (
  SELECT s.*
  FROM (
      SELECT t.transaction_id,
        d.document_id,
        'customer_ns_exact_amount' AS seed
      FROM transactions_scope t
        JOIN document_scope d ON d.company_id = t.company_id::uuid
        AND (
          (NOT v_directional_matching)
          OR t.tx_is_in
        )
        AND d.currency = t.currency
        AND d.doc_customer_ns IS NOT NULL
        AND t.tx_customer_ns IS NOT NULL
        AND (
          d.doc_customer_ns = t.tx_customer_ns
          OR position(d.doc_customer_ns in t.tx_customer_ns) > 0
          OR position(t.tx_customer_ns in d.doc_customer_ns) > 0
        )
        AND t.credited IS NOT NULL
        AND t.credited > 0
        AND d.total = t.credited
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
    ) s
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s4_merchant_id_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s5_merchant_ns_exact_amount
    ) done USING (transaction_id)
  WHERE done.transaction_id IS NULL
),
s7_iban_plus_signal AS (
  SELECT t.transaction_id,
    d_sub.document_id,
    'iban_plus_signal' AS seed
  FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s4_merchant_id_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s5_merchant_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s6_customer_ns_exact_amount
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
        AND (
          (
            (
              (NOT v_directional_matching)
              OR t.tx_is_out
            )
            AND (
              (
                d.doc_iban_ns IS NOT NULL
                AND t.tx_iban_counterparty_ns IS NOT NULL
                AND d.doc_iban_ns = t.tx_iban_counterparty_ns
              )
              OR (
                d.doc_iban_core_ns IS NOT NULL
                AND t.tx_iban_counterparty_core_ns IS NOT NULL
                AND d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns
              )
            )
          )
          OR (
            (
              (NOT v_directional_matching)
              OR t.tx_is_in
            )
            AND (
              (
                d.doc_iban_ns IS NOT NULL
                AND t.tx_iban_beneficiary_ns IS NOT NULL
                AND d.doc_iban_ns = t.tx_iban_beneficiary_ns
              )
              OR (
                d.doc_iban_core_ns IS NOT NULL
                AND t.tx_iban_beneficiary_core_ns IS NOT NULL
                AND d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns
              )
            )
          )
        )
        AND (
          (
            t.invoice_nr_norm IS NOT NULL
            AND d.invoice_number_norm IS NOT NULL
          )
          OR (
            t.order_id_norm IS NOT NULL
            AND d.order_id_norm IS NOT NULL
          )
          OR (
            t.tx_merchant_ns IS NOT NULL
            AND d.doc_merchant_ns IS NOT NULL
          )
          OR (
            t.tx_customer_ns IS NOT NULL
            AND d.doc_customer_ns IS NOT NULL
          )
        )
      ORDER BY d.document_date_unix,
        d.document_id
      LIMIT v_cap_precise
    ) d_sub ON TRUE
  WHERE done.transaction_id IS NULL
),
s8_merchant_id_tol AS (
  SELECT t.transaction_id,
    d_sub.document_id,
    'merchant_id_tol' AS seed
  FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s4_merchant_id_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s5_merchant_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s6_customer_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s7_iban_plus_signal
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          (NOT v_directional_matching)
          OR t.tx_is_out
        )
        AND d.currency = t.currency
        AND d.merchant_id = t.merchant_id
        AND v_amount_tolerance_fraction > 0
        AND t.debited IS NOT NULL
        AND t.debited > 0
        AND d.total BETWEEN (t.debited / (1 + v_amount_tolerance_fraction)) AND (t.debited / (1 - v_amount_tolerance_fraction))
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
      ORDER BY d.document_date_unix,
        d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
  WHERE done.transaction_id IS NULL
),
s9_merchant_ns_tol AS (
  SELECT t.transaction_id,
    d_sub.document_id,
    'merchant_ns_tol' AS seed
  FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s4_merchant_id_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s5_merchant_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s6_customer_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s7_iban_plus_signal
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s8_merchant_id_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          (NOT v_directional_matching)
          OR t.tx_is_out
        )
        AND d.currency = t.currency
        AND d.doc_merchant_ns IS NOT NULL
        AND t.tx_merchant_ns IS NOT NULL
        AND (
          d.doc_merchant_ns = t.tx_merchant_ns
          OR position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
          OR position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
        )
        AND v_amount_tolerance_fraction > 0
        AND t.debited IS NOT NULL
        AND t.debited > 0
        AND d.total BETWEEN (t.debited / (1 + v_amount_tolerance_fraction)) AND (t.debited / (1 - v_amount_tolerance_fraction))
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
      ORDER BY d.document_date_unix,
        d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
  WHERE done.transaction_id IS NULL
),
s10_customer_ns_tol AS (
  SELECT t.transaction_id,
    d_sub.document_id,
    'customer_ns_tol' AS seed
  FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s4_merchant_id_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s5_merchant_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s6_customer_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s7_iban_plus_signal
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s8_merchant_id_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s9_merchant_ns_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          (NOT v_directional_matching)
          OR t.tx_is_in
        )
        AND d.currency = t.currency
        AND d.doc_customer_ns IS NOT NULL
        AND t.tx_customer_ns IS NOT NULL
        AND (
          d.doc_customer_ns = t.tx_customer_ns
          OR position(d.doc_customer_ns in t.tx_customer_ns) > 0
          OR position(t.tx_customer_ns in d.doc_customer_ns) > 0
        )
        AND v_amount_tolerance_fraction > 0
        AND t.credited IS NOT NULL
        AND t.credited > 0
        AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction)) AND (t.credited / (1 - v_amount_tolerance_fraction))
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
      ORDER BY d.document_date_unix,
        d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
  WHERE done.transaction_id IS NULL
),
s11_iban_tol AS (
  SELECT t.transaction_id,
    d_sub.document_id,
    'iban_tol' AS seed
  FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s4_merchant_id_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s5_merchant_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s6_customer_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s7_iban_plus_signal
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s8_merchant_id_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s9_merchant_ns_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s10_customer_ns_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
        AND v_amount_tolerance_fraction > 0
        AND (
          (
            (
              (NOT v_directional_matching)
              OR t.tx_is_out
            )
            AND (
              (
                d.doc_iban_ns IS NOT NULL
                AND t.tx_iban_counterparty_ns IS NOT NULL
                AND d.doc_iban_ns = t.tx_iban_counterparty_ns
              )
              OR (
                d.doc_iban_core_ns IS NOT NULL
                AND t.tx_iban_counterparty_core_ns IS NOT NULL
                AND d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns
              )
            )
            AND t.debited IS NOT NULL
            AND t.debited > 0
            AND d.total BETWEEN (t.debited / (1 + v_amount_tolerance_fraction)) AND (t.debited / (1 - v_amount_tolerance_fraction))
          )
          OR (
            (
              (NOT v_directional_matching)
              OR t.tx_is_in
            )
            AND (
              (
                d.doc_iban_ns IS NOT NULL
                AND t.tx_iban_beneficiary_ns IS NOT NULL
                AND d.doc_iban_ns = t.tx_iban_beneficiary_ns
              )
              OR (
                d.doc_iban_core_ns IS NOT NULL
                AND t.tx_iban_beneficiary_core_ns IS NOT NULL
                AND d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns
              )
            )
            AND t.credited IS NOT NULL
            AND t.credited > 0
            AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction)) AND (t.credited / (1 - v_amount_tolerance_fraction))
          )
        )
      ORDER BY d.document_date_unix,
        d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
  WHERE done.transaction_id IS NULL
),
s12_window_amount_exact AS (
  SELECT t.transaction_id,
    d_sub.document_id,
    'window_amount_exact' AS seed
  FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s4_merchant_id_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s5_merchant_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s6_customer_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s7_iban_plus_signal
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s8_merchant_id_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s9_merchant_ns_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s10_customer_ns_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s11_iban_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
        AND (
          (
            (
              (NOT v_directional_matching)
              OR t.tx_is_out
            )
            AND t.debited IS NOT NULL
            AND t.debited > 0
            AND d.total = t.debited
          )
          OR (
            (
              (NOT v_directional_matching)
              OR t.tx_is_in
            )
            AND t.credited IS NOT NULL
            AND t.credited > 0
            AND d.total = t.credited
          )
        )
      ORDER BY d.document_date_unix,
        d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
  WHERE done.transaction_id IS NULL
),
s13_window_amount_tol AS (
  SELECT t.transaction_id,
    d_sub.document_id,
    'window_amount_tol' AS seed
  FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s4_merchant_id_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s5_merchant_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s6_customer_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s7_iban_plus_signal
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s8_merchant_id_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s9_merchant_ns_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s10_customer_ns_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s11_iban_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s12_window_amount_exact
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
        AND v_amount_tolerance_fraction > 0
        AND (
          (
            (
              (NOT v_directional_matching)
              OR t.tx_is_out
            )
            AND t.debited IS NOT NULL
            AND t.debited > 0
            AND d.total BETWEEN (t.debited / (1 + v_amount_tolerance_fraction)) AND (t.debited / (1 - v_amount_tolerance_fraction))
          )
          OR (
            (
              (NOT v_directional_matching)
              OR t.tx_is_in
            )
            AND t.credited IS NOT NULL
            AND t.credited > 0
            AND d.total BETWEEN (t.credited / (1 + v_amount_tolerance_fraction)) AND (t.credited / (1 - v_amount_tolerance_fraction))
          )
        )
      ORDER BY d.document_date_unix,
        d.document_id
      LIMIT v_cap_broad
    ) d_sub ON TRUE
  WHERE done.transaction_id IS NULL
),
s14_window_only AS (
  SELECT t.transaction_id,
    d_sub.document_id,
    'window_only' AS seed
  FROM transactions_scope t
    LEFT JOIN (
      SELECT DISTINCT transaction_id
      FROM s1_invoice
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s2_order
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s3_iban_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s4_merchant_id_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s5_merchant_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s6_customer_ns_exact_amount
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s7_iban_plus_signal
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s8_merchant_id_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s9_merchant_ns_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s10_customer_ns_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s11_iban_tol
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s12_window_amount_exact
      UNION ALL
      SELECT DISTINCT transaction_id
      FROM s13_window_amount_tol
    ) done USING (transaction_id)
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND d.currency = t.currency
        AND d.document_date_unix IS NOT NULL
        AND t.completed_date IS NOT NULL
        AND d.document_date_unix BETWEEN (t.completed_date - v_date_window_seconds) AND (t.completed_date + v_date_window_seconds)
      ORDER BY d.document_date_unix,
        d.document_id
      LIMIT v_cap_window_only
    ) d_sub ON TRUE
  WHERE done.transaction_id IS NULL
),
seeds AS (
  SELECT *
  FROM s1_invoice
  UNION ALL
  SELECT *
  FROM s2_order
  UNION ALL
  SELECT *
  FROM s3_iban_exact_amount
  UNION ALL
  SELECT *
  FROM s4_merchant_id_exact_amount
  UNION ALL
  SELECT *
  FROM s5_merchant_ns_exact_amount
  UNION ALL
  SELECT *
  FROM s6_customer_ns_exact_amount
  UNION ALL
  SELECT *
  FROM s7_iban_plus_signal
  UNION ALL
  SELECT *
  FROM s8_merchant_id_tol
  UNION ALL
  SELECT *
  FROM s9_merchant_ns_tol
  UNION ALL
  SELECT *
  FROM s10_customer_ns_tol
  UNION ALL
  SELECT *
  FROM s11_iban_tol
  UNION ALL
  SELECT *
  FROM s12_window_amount_exact
  UNION ALL
  SELECT *
  FROM s13_window_amount_tol
  UNION ALL
  SELECT *
  FROM s14_window_only
),
seed_counts_total AS (
  SELECT seed,
    COUNT(*) AS ct
  FROM seeds
  GROUP BY seed
),
seed_counts_distinct AS (
  SELECT seed,
    COUNT(DISTINCT (transaction_id, document_id)) AS ct
  FROM seeds
  GROUP BY seed
),
candidate_pairs AS (
  SELECT DISTINCT transaction_id,
    document_id
  FROM seeds
),
/* ---------------- scoring ---------------- */
distinct_pairs AS (
  SELECT DISTINCT transaction_id,
    document_id
  FROM candidate_pairs
),
attributes AS (
  SELECT p.transaction_id,
    p.document_id,
    t.company_id::uuid AS transaction_company_id,
    d.company_id AS document_company_id,
    t.amount AS transaction_amount,
    t.debited AS transaction_debited,
    t.credited AS transaction_credited,
    d.total AS document_total,
    t.currency AS transaction_currency,
    d.currency AS document_currency,
    t.invoice_nr_norm AS tx_invoice_norm,
    d.invoice_number_norm AS doc_invoice_norm,
    t.order_id_norm AS tx_order_norm,
    d.order_id_norm AS doc_order_norm,
    d.invoice_number_raw AS doc_invoice_raw,
    d.order_id_raw AS doc_order_raw,
    t.completed_date AS transaction_date_unix,
    d.document_date_unix AS document_date_unix,
    t.merchant_id AS transaction_merchant_id,
    d.merchant_id AS document_merchant_id,
    t.tx_customer_display AS transaction_customer_name,
    d.doc_customer_display AS document_customer_name,
    t.tx_is_in,
    t.tx_is_out,
    t.tx_merchant_ns,
    d.doc_merchant_ns,
    t.tx_customer_ns,
    d.doc_customer_ns,
    t.tx_iban_beneficiary_ns,
    t.tx_iban_counterparty_ns,
    d.doc_iban_ns,
    t.tx_iban_beneficiary_core_ns,
    t.tx_iban_counterparty_core_ns,
    d.doc_iban_core_ns,
    COALESCE(NULLIF(d.doc_dedup_key, ''), d.document_id::text) AS doc_dedup_key,
    t.effective_amount AS transaction_effective_amount
  FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope d ON d.document_id = p.document_id
  WHERE t.company_id::uuid = d.company_id
),
evaluated AS (
  SELECT a.transaction_id,
    a.document_id,
    a.transaction_effective_amount,
    COALESCE(
      (
        a.tx_invoice_norm IS NOT NULL
        AND a.doc_invoice_norm IS NOT NULL
        AND a.tx_invoice_norm = a.doc_invoice_norm
      ),
      FALSE
    ) AS invoice_number_match,
    COALESCE(
      (
        a.tx_order_norm IS NOT NULL
        AND a.doc_order_norm IS NOT NULL
        AND a.tx_order_norm = a.doc_order_norm
      ),
      FALSE
    ) AS order_number_match,
    COALESCE(
      a.transaction_currency = a.document_currency,
      FALSE
    ) AS currency_match,
    (
      CASE
        WHEN v_directional_matching THEN (
          a.tx_is_out
          AND (
            (
              a.transaction_merchant_id IS NOT NULL
              AND a.document_merchant_id IS NOT NULL
              AND a.transaction_merchant_id = a.document_merchant_id
            )
            OR (
              a.tx_merchant_ns IS NOT NULL
              AND a.doc_merchant_ns IS NOT NULL
              AND (
                a.tx_merchant_ns = a.doc_merchant_ns
                OR position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                OR position(a.tx_merchant_ns in a.doc_merchant_ns) > 0
              )
            )
            OR (
              (
                a.tx_iban_counterparty_ns IS NOT NULL
                AND a.doc_iban_ns IS NOT NULL
                AND a.tx_iban_counterparty_ns = a.doc_iban_ns
              )
              OR (
                a.tx_iban_counterparty_core_ns IS NOT NULL
                AND a.doc_iban_core_ns IS NOT NULL
                AND a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns
              )
            )
          )
        )
        ELSE (
          (
            a.transaction_merchant_id IS NOT NULL
            AND a.document_merchant_id IS NOT NULL
            AND a.transaction_merchant_id = a.document_merchant_id
          )
          OR (
            a.tx_merchant_ns IS NOT NULL
            AND a.doc_merchant_ns IS NOT NULL
            AND (
              a.tx_merchant_ns = a.doc_merchant_ns
              OR position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
              OR position(a.tx_merchant_ns in a.doc_merchant_ns) > 0
            )
          )
          OR (
            (
              a.tx_iban_counterparty_ns IS NOT NULL
              AND a.doc_iban_ns IS NOT NULL
              AND a.tx_iban_counterparty_ns = a.doc_iban_ns
            )
            OR (
              a.tx_iban_counterparty_core_ns IS NOT NULL
              AND a.doc_iban_core_ns IS NOT NULL
              AND a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns
            )
          )
        )
      END
    ) AS merchant_match,
    (
      CASE
        WHEN v_directional_matching THEN (
          a.tx_is_in
          AND (
            (
              a.tx_customer_ns IS NOT NULL
              AND a.doc_customer_ns IS NOT NULL
              AND (
                a.tx_customer_ns = a.doc_customer_ns
                OR position(a.doc_customer_ns in a.tx_customer_ns) > 0
                OR position(a.tx_customer_ns in a.doc_customer_ns) > 0
              )
            )
            OR (
              (
                a.tx_iban_beneficiary_ns IS NOT NULL
                AND a.doc_iban_ns IS NOT NULL
                AND a.tx_iban_beneficiary_ns = a.doc_iban_ns
              )
              OR (
                a.tx_iban_beneficiary_core_ns IS NOT NULL
                AND a.doc_iban_core_ns IS NOT NULL
                AND a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns
              )
            )
          )
        )
        ELSE (
          (
            a.tx_customer_ns IS NOT NULL
            AND a.doc_customer_ns IS NOT NULL
            AND (
              a.tx_customer_ns = a.doc_customer_ns
              OR position(a.doc_customer_ns in a.tx_customer_ns) > 0
              OR position(a.tx_customer_ns in a.doc_customer_ns) > 0
            )
          )
          OR (
            (
              a.tx_iban_beneficiary_ns IS NOT NULL
              AND a.doc_iban_ns IS NOT NULL
              AND a.tx_iban_beneficiary_ns = a.doc_iban_ns
            )
            OR (
              a.tx_iban_beneficiary_core_ns IS NOT NULL
              AND a.doc_iban_core_ns IS NOT NULL
              AND a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns
            )
          )
        )
      END
    ) AS customer_match,
    COALESCE(
      ABS(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds,
      FALSE
    ) AS date_within_cap,
    a.document_total,
    a.document_company_id,
    a.transaction_date_unix,
    a.document_date_unix,
    a.doc_dedup_key,
    (
      a.transaction_debited IS NOT NULL
      AND a.transaction_debited > 0
      AND a.document_total = a.transaction_debited
    ) AS amount_exact_from_debited,
    (
      a.transaction_credited IS NOT NULL
      AND a.transaction_credited > 0
      AND a.document_total = a.transaction_credited
    ) AS amount_exact_from_credited,
    (
      a.transaction_debited IS NOT NULL
      AND a.transaction_debited > 0
      AND ABS(a.document_total - a.transaction_debited) <= a.document_total * v_amount_tolerance_fraction
    ) AS amount_tolerance_from_debited,
    (
      a.transaction_credited IS NOT NULL
      AND a.transaction_credited > 0
      AND ABS(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction
    ) AS amount_tolerance_from_credited,
    a.tx_is_in,
    a.tx_is_out,
    -- carry for summary
    a.tx_iban_beneficiary_ns,
    a.tx_iban_counterparty_ns,
    a.doc_iban_ns,
    a.tx_iban_beneficiary_core_ns,
    a.tx_iban_counterparty_core_ns,
    a.doc_iban_core_ns,
    a.tx_merchant_ns,
    a.doc_merchant_ns,
    a.tx_customer_ns,
    a.doc_customer_ns,
    a.transaction_merchant_id,
    a.document_merchant_id
  FROM attributes a
),
with_amount_tolerance AS (
  SELECT e.*,
    CASE
      WHEN e.tx_is_out
      AND e.amount_exact_from_debited THEN TRUE
      WHEN e.tx_is_in
      AND e.amount_exact_from_credited THEN TRUE
      WHEN NOT v_directional_matching
      AND (
        e.amount_exact_from_debited
        OR e.amount_exact_from_credited
      ) THEN TRUE
      ELSE FALSE
    END AS amount_exact_match,
    CASE
      WHEN v_amount_tolerance_fraction > 0 THEN CASE
        WHEN e.tx_is_out
        AND e.amount_tolerance_from_debited THEN TRUE
        WHEN e.tx_is_in
        AND e.amount_tolerance_from_credited THEN TRUE
        WHEN NOT v_directional_matching
        AND (
          e.amount_tolerance_from_debited
          OR e.amount_tolerance_from_credited
        ) THEN TRUE
        ELSE FALSE
      END
      ELSE FALSE
    END AS amount_tolerance_match,
    (
      e.amount_exact_from_debited
      OR e.amount_exact_from_credited
      OR e.amount_tolerance_from_debited
      OR e.amount_tolerance_from_credited
    ) AS amount_direction_ok
  FROM evaluated e
),
resolved_weights AS (
  SELECT COALESCE((v_weights_json->>'amount_total')::numeric, 0) AS amount_weight,
    COALESCE((v_weights_json->>'invoice_number')::numeric, 0) AS invoice_weight,
    COALESCE((v_weights_json->>'order_number')::numeric, 0) AS order_weight,
    COALESCE((v_weights_json->>'date')::numeric, 0) AS date_weight,
    COALESCE((v_weights_json->>'currency')::numeric, 0) AS currency_weight,
    COALESCE((v_weights_json->>'merchant_id')::numeric, 0) AS merchant_weight,
    COALESCE((v_weights_json->>'customer_name')::numeric, 0) AS customer_weight
),
scored_components AS (
  SELECT m.transaction_id,
    m.document_id,
    m.document_company_id,
    m.doc_dedup_key,
    m.transaction_date_unix,
    m.document_date_unix,
    m.amount_direction_ok,
    m.tx_is_in,
    m.tx_is_out,
    -- carry fields for summary
    m.tx_iban_beneficiary_ns,
    m.tx_iban_counterparty_ns,
    m.doc_iban_ns,
    m.tx_iban_beneficiary_core_ns,
    m.tx_iban_counterparty_core_ns,
    m.doc_iban_core_ns,
    m.tx_merchant_ns,
    m.doc_merchant_ns,
    m.tx_customer_ns,
    m.doc_customer_ns,
    m.transaction_merchant_id,
    m.document_merchant_id,
    COALESCE(
      CASE
        WHEN m.amount_exact_match THEN w.amount_weight
        WHEN m.amount_tolerance_match THEN w.amount_weight * EXP(
          -0.5 * POWER(
            (
              ABS(
                m.transaction_effective_amount - m.document_total
              )
            ) / NULLIF(
              (m.document_total * v_amount_tolerance_fraction) / v_gauss_k,
              0
            ),
            2
          )
        )
        ELSE 0
      END,
      0
    )::numeric AS amount_score,
    (
      m.amount_exact_match
      OR m.amount_tolerance_match
    ) AS amount_match,
    m.invoice_number_match AS invoice_number_match,
    m.order_number_match AS order_number_match,
    CASE
      WHEN m.transaction_date_unix IS NOT NULL
      AND m.document_date_unix IS NOT NULL THEN w.date_weight * EXP(
        -0.5 * POWER(
          GREATEST(
            0,
            (
              ABS(m.transaction_date_unix - m.document_date_unix) / 86400.0
            ) - v_date_full_weight_grace_days
          ) / NULLIF(
            GREATEST(
              1.0,
              (
                v_config_date_window_days - v_date_full_weight_grace_days
              ) / v_gauss_k
            ),
            0
          ),
          2
        )
      )
      ELSE 0
    END::numeric AS date_score,
    m.date_within_cap AS date_match,
    m.currency_match AS currency_match,
    m.merchant_match AS merchant_match,
    m.customer_match AS customer_match,
    w.amount_weight AS amount_weight,
    w.invoice_weight AS invoice_weight,
    w.order_weight AS order_weight,
    w.date_weight AS date_weight,
    w.currency_weight AS currency_weight,
    w.merchant_weight AS merchant_weight,
    w.customer_weight AS customer_weight,
    (
      COALESCE(m.invoice_number_match::int, 0) * w.invoice_weight
    )::numeric AS invoice_score,
    (
      COALESCE(m.order_number_match::int, 0) * w.order_weight
    )::numeric AS order_score,
    (
      COALESCE(m.currency_match::int, 0) * w.currency_weight
    )::numeric AS currency_score,
    (
      COALESCE(m.merchant_match::int, 0) * w.merchant_weight
    )::numeric AS merchant_score,
    (
      COALESCE(m.customer_match::int, 0) * w.customer_weight
    )::numeric AS customer_score,
    m.transaction_effective_amount,
    m.document_total
  FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
),
scored_with_applicability AS (
  SELECT s.*,
    (
      CASE
        WHEN v_directional_matching THEN (
          s.tx_is_out
          AND (
            (
              s.doc_merchant_ns IS NOT NULL
              AND s.tx_merchant_ns IS NOT NULL
            )
            OR (
              (
                s.doc_iban_ns IS NOT NULL
                AND s.tx_iban_counterparty_ns IS NOT NULL
                AND s.doc_iban_ns = s.tx_iban_counterparty_ns
              )
              OR (
                s.doc_iban_core_ns IS NOT NULL
                AND s.tx_iban_counterparty_core_ns IS NOT NULL
                AND s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns
              )
            )
            OR (
              s.document_merchant_id IS NOT NULL
              AND s.transaction_merchant_id IS NOT NULL
            )
          )
        )
        ELSE (
          (
            s.doc_merchant_ns IS NOT NULL
            AND s.tx_merchant_ns IS NOT NULL
          )
          OR (
            (
              s.doc_iban_ns IS NOT NULL
              AND s.tx_iban_counterparty_ns IS NOT NULL
              AND s.doc_iban_ns = s.tx_iban_counterparty_ns
            )
            OR (
              s.doc_iban_core_ns IS NOT NULL
              AND s.tx_iban_counterparty_core_ns IS NOT NULL
              AND s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns
            )
          )
          OR (
            s.document_merchant_id IS NOT NULL
            AND s.transaction_merchant_id IS NOT NULL
          )
        )
      END
    ) AS merchant_applicable,
    (
      CASE
        WHEN v_directional_matching THEN (
          s.tx_is_in
          AND (
            (
              s.doc_customer_ns IS NOT NULL
              AND s.tx_customer_ns IS NOT NULL
            )
            OR (
              (
                s.doc_iban_ns IS NOT NULL
                AND s.tx_iban_beneficiary_ns IS NOT NULL
                AND s.doc_iban_ns = s.tx_iban_beneficiary_ns
              )
              OR (
                s.doc_iban_core_ns IS NOT NULL
                AND s.tx_iban_beneficiary_core_ns IS NOT NULL
                AND s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns
              )
            )
          )
        )
        ELSE (
          (
            s.doc_customer_ns IS NOT NULL
            AND s.tx_customer_ns IS NOT NULL
          )
          OR (
            (
              s.doc_iban_ns IS NOT NULL
              AND s.tx_iban_beneficiary_ns IS NOT NULL
              AND s.doc_iban_ns = s.tx_iban_beneficiary_ns
            )
            OR (
              s.doc_iban_core_ns IS NOT NULL
              AND s.tx_iban_beneficiary_core_ns IS NOT NULL
              AND s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns
            )
          )
        )
      END
    ) AS customer_applicable
  FROM scored_components s
),
scored_with_confidence AS (
  SELECT s.*,
    (
      s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight + (
        CASE
          WHEN s.merchant_applicable THEN s.merchant_weight
          ELSE 0
        END
      ) + (
        CASE
          WHEN s.customer_applicable THEN s.customer_weight
          ELSE 0
        END
      )
    )::numeric AS dynamic_total_weight,
    (
      s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + (
        CASE
          WHEN s.merchant_applicable THEN s.merchant_score
          ELSE 0
        END
      ) + (
        CASE
          WHEN s.customer_applicable THEN s.customer_score
          ELSE 0
        END
      )
    )::numeric AS total_numerator,
    (
      COALESCE(s.amount_match::int, 0) + COALESCE(s.invoice_number_match::int, 0) + COALESCE(s.order_number_match::int, 0) + COALESCE(s.date_match::int, 0) + COALESCE(s.currency_match::int, 0) + COALESCE(s.merchant_match::int, 0) + COALESCE(s.customer_match::int, 0)
    ) AS signals_matched_count,
    CASE
      WHEN (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight + (
          CASE
            WHEN s.merchant_applicable THEN s.merchant_weight
            ELSE 0
          END
        ) + (
          CASE
            WHEN s.customer_applicable THEN s.customer_weight
            ELSE 0
          END
        )
      ) > 0 THEN ROUND(
        (
          (
            s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + (
              CASE
                WHEN s.merchant_applicable THEN s.merchant_score
                ELSE 0
              END
            ) + (
              CASE
                WHEN s.customer_applicable THEN s.customer_score
                ELSE 0
              END
            )
          ) / (
            s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight + (
              CASE
                WHEN s.merchant_applicable THEN s.merchant_weight
                ELSE 0
              END
            ) + (
              CASE
                WHEN s.customer_applicable THEN s.customer_weight
                ELSE 0
              END
            )
          )
        ) * 100.0,
        4
      )
      ELSE 0
    END AS confidence_percent
  FROM scored_with_applicability s
),
low_signal AS (
  SELECT *
  FROM scored_with_confidence
  WHERE signals_matched_count < 1
),
below_min_confidence AS (
  SELECT *
  FROM scored_with_confidence
  WHERE signals_matched_count >= 1
    AND confidence_percent < v_config_min_confidence
),
base_candidates AS (
  SELECT *
  FROM scored_with_confidence
  WHERE signals_matched_count >= 1
    AND confidence_percent >= v_config_min_confidence
),
ranked_per_transaction AS (
  SELECT b.*,
    ROW_NUMBER() OVER (
      PARTITION BY b.transaction_id
      ORDER BY b.confidence_percent DESC,
        b.total_numerator DESC,
        b.signals_matched_count DESC,
        b.document_id
    ) AS rownum_per_transaction
  FROM base_candidates b
),
not_top_for_transaction AS (
  SELECT *
  FROM ranked_per_transaction
  WHERE rownum_per_transaction > 1
),
top_per_transaction AS (
  SELECT *
  FROM ranked_per_transaction
  WHERE rownum_per_transaction = 1
),
ranked_per_document AS (
  SELECT t.*,
    ROW_NUMBER() OVER (
      PARTITION BY t.doc_dedup_key
      ORDER BY t.confidence_percent DESC,
        t.total_numerator DESC,
        t.signals_matched_count DESC,
        ABS(t.transaction_date_unix - t.document_date_unix) ASC,
        t.document_id
    ) AS rownum_per_document
  FROM top_per_transaction t
),
not_top_for_document AS (
  SELECT *
  FROM ranked_per_document
  WHERE rownum_per_document > 1
),
top_per_document AS (
  SELECT *
  FROM ranked_per_document
  WHERE rownum_per_document = 1
),
final_candidates AS (
  SELECT *
  FROM top_per_document
  ORDER BY confidence_percent DESC,
    total_numerator DESC,
    signals_matched_count DESC,
    transaction_id,
    document_id
),
allowed_for_insert AS (
  SELECT f.*
  FROM final_candidates f
  WHERE NOT EXISTS (
      SELECT 1
      FROM public.reconciliations r1
      WHERE r1.transaction_id = f.transaction_id
    )
    AND NOT EXISTS (
      SELECT 1
      FROM public.reconciliations r2
      WHERE r2.document_id = f.document_id
    )
),
inserted_reconciliations AS (
  INSERT INTO public.reconciliations (
      transaction_id,
      document_id,
      company_id,
      confidence,
      time,
      engine_version,
      fiscal_year,
      quarter,
      month,
      summary
    ) WITH ts_base AS (
      SELECT f.transaction_id,
        f.document_id,
        f.document_company_id,
        f.confidence_percent,
        COALESCE(
          f.transaction_date_unix,
          f.document_date_unix,
          v_log_run_unix_time
        )::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT d.transaction_id,
        d.document_id,
        d.document_company_id,
        d.confidence_percent,
        d.invoice_number_match AS invoice_match,
        d.order_number_match AS order_match,
        d.amount_match,
        d.date_match,
        d.currency_match,
        d.merchant_match,
        d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        t.base_unix,
        a.tx_invoice_norm,
        a.doc_invoice_norm,
        a.tx_order_norm,
        a.doc_order_norm,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name,
        a.doc_invoice_raw,
        a.doc_order_raw,
        a.tx_iban_beneficiary_ns,
        a.tx_iban_counterparty_ns,
        a.doc_iban_ns,
        a.tx_iban_beneficiary_core_ns,
        a.tx_iban_counterparty_core_ns,
        a.doc_iban_core_ns,
        a.tx_is_in,
        a.tx_is_out
      FROM ts_base t
        JOIN final_candidates d ON d.transaction_id = t.transaction_id
        AND d.document_id = t.document_id
        JOIN attributes a ON a.transaction_id = d.transaction_id
        AND a.document_id = d.document_id
    )
  SELECT e.transaction_id,
    e.document_id,
    e.document_company_id,
    COALESCE(e.confidence_percent, 0)::numeric,
    v_log_run_unix_time,
    v_engine_version,
    EXTRACT(
      YEAR
      FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC')
    )::int,
    EXTRACT(
      QUARTER
      FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC')
    )::int,
    to_char(
      (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'),
      'FMMonth'
    ),
    json_build_object(
      'signals',
      (
        SELECT json_agg(s)
        FROM unnest(
            ARRAY [
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]
          ) s
        WHERE s IS NOT NULL
      ),
      'tx_amount',
      e.transaction_effective_amount,
      'doc_total',
      e.document_total,
      'tx_invoice',
      e.tx_invoice_norm,
      'doc_invoice',
      COALESCE(e.doc_invoice_raw, e.doc_invoice_norm),
      'tx_order',
      e.tx_order_norm,
      'doc_order',
      COALESCE(e.doc_order_raw, e.doc_order_norm),
      'tx_currency',
      e.transaction_currency,
      'doc_currency',
      e.document_currency,
      'tx_merchant',
      e.transaction_merchant_id,
      'doc_merchant',
      e.document_merchant_id,
      'tx_customer',
      e.transaction_customer_name,
      'doc_customer',
      e.document_customer_name,
      'tx_unix',
      e.transaction_date_unix,
      'doc_unix',
      e.document_date_unix,
      'tx_iban_beneficiary',
      e.tx_iban_beneficiary_ns,
      'tx_iban_counterparty',
      e.tx_iban_counterparty_ns,
      'doc_iban',
      e.doc_iban_ns,
      'tx_iban_beneficiary_core',
      e.tx_iban_beneficiary_core_ns,
      'tx_iban_counterparty_core',
      e.tx_iban_counterparty_core_ns,
      'doc_iban_core',
      e.doc_iban_core_ns,
      'tx_is_in',
      e.tx_is_in,
      'tx_is_out',
      e.tx_is_out,
      'direction',
      CASE
        WHEN e.tx_is_in THEN 'incoming'
        WHEN e.tx_is_out THEN 'outgoing'
        ELSE 'unknown'
      END,
      'days_between_tx_and_doc',
      CASE
        WHEN e.transaction_date_unix IS NOT NULL
        AND e.document_date_unix IS NOT NULL THEN (
          ABS(e.transaction_date_unix - e.document_date_unix) / 86400
        )::int
        ELSE NULL
      END
    )::text
  FROM enrich e
  RETURNING reconciliation_id,
    transaction_id,
    document_id,
    confidence
),
inserted_match_scores AS (
  INSERT INTO public.reconciliations_match_scores (
      reconciliation_id,
      company_id,
      confidence,
      numerator,
      total_weight,
      date_match,
      invoice_match,
      order_match,
      merchant_match,
      amount_match,
      currency_match,
      customer_match,
      date_weight,
      invoice_weight,
      order_weight,
      merchant_weight,
      amount_weight,
      currency_weight,
      customer_weight,
      date_score,
      invoice_score,
      order_score,
      merchant_score,
      amount_score,
      currency_score,
      customer_score
    )
  SELECT r.reconciliation_id,
    d.document_company_id AS company_id,
    r.confidence,
    d.total_numerator,
    d.dynamic_total_weight,
    d.date_match,
    d.invoice_number_match,
    d.order_number_match,
    d.merchant_match,
    (d.amount_match)::bool,
    d.currency_match,
    d.customer_match,
    d.date_weight,
    d.invoice_weight,
    d.order_weight,
    d.merchant_weight,
    d.amount_weight,
    d.currency_weight,
    d.customer_weight,
    d.date_score,
    d.invoice_score,
    d.order_score,
    d.merchant_score,
    d.amount_score,
    d.currency_score,
    d.customer_score
  FROM inserted_reconciliations r
    JOIN scored_with_confidence d ON d.transaction_id = r.transaction_id
    AND d.document_id = r.document_id
  RETURNING 1
),
final_counts AS (
  SELECT (
      SELECT COUNT(*)
      FROM top_per_transaction
    ) AS top_per_transaction_count,
    (
      SELECT COUNT(*)
      FROM top_per_document
    ) AS top_per_document_count,
    (
      SELECT COUNT(*)
      FROM final_candidates
    ) AS passed_confidence_count,
    (
      SELECT COUNT(*)
      FROM inserted_reconciliations
    ) AS inserted_count,
    (
      SELECT COUNT(*)
      FROM base_candidates
    ) AS candidates_count,
    (
      SELECT COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb)
      FROM seed_counts_total
    ) AS seed_counts_total_json,
    (
      SELECT COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb)
      FROM seed_counts_distinct
    ) AS seed_counts_distinct_json,
    (
      SELECT COUNT(*)
      FROM transactions_scope
    ) AS total_tx_count,
    (
      SELECT COUNT(*)
      FROM document_scope
    ) AS total_doc_count
)
SELECT top_per_transaction_count,
  top_per_document_count,
  passed_confidence_count,
  inserted_count,
  candidates_count,
  seed_counts_total_json,
  seed_counts_distinct_json,
  total_tx_count,
  total_doc_count INTO v_summary_top_per_tx,
  v_summary_top_per_doc,
  v_summary_passed_count,
  v_inserted_count,
  v_candidates_count,
  v_seed_counts_total,
  v_seed_counts_distinct,
  v_total_tx,
  v_total_docs
FROM final_counts;
-- end log
v_elapsed_ms := FLOOR(
  EXTRACT(
    EPOCH
    FROM (clock_timestamp() - v_start_time)
  ) * 1000
)::bigint;
INSERT INTO public.reconciliations_logs (
    run_id,
    function_version,
    action,
    target,
    status,
    message,
    company_id,
    transaction_id,
    document_id,
    reconciliation_id,
    rows_affected,
    confidence,
    step_ms,
    processing_ms,
    context
  )
VALUES (
    v_log_run_id,
    v_engine_version,
    'run_end',
    'function',
    'ok',
    NULL,
    p_company_id,
    NULL,
    NULL,
    NULL,
    v_inserted_count,
    NULL,
    NULL,
    v_elapsed_ms,
    jsonb_build_object(
      'inserted',
      v_inserted_count,
      'candidates',
      v_candidates_count,
      'transactions_scanned',
      v_total_tx,
      'documents_considered',
      v_total_docs,
      'top_per_tx',
      v_summary_top_per_tx,
      'top_per_doc',
      v_summary_top_per_doc,
      'passed_confidence',
      v_summary_passed_count,
      'seed_candidates_by_logic',
      v_seed_counts_total,
      'seed_unique_pairs_by_logic',
      v_seed_counts_distinct
    )
  );
RETURN jsonb_build_object(
  'msg',
  v_status_message,
  'time_ms',
  v_elapsed_ms,
  'transactions_scanned',
  v_total_tx,
  'documents_considered',
  v_total_docs,
  'top_per_tx',
  v_summary_top_per_tx,
  'top_per_doc',
  v_summary_top_per_doc,
  'passed_confidence',
  v_summary_passed_count,
  'seed_candidates_by_logic',
  v_seed_counts_total,
  'seed_unique_pairs_by_logic',
  v_seed_counts_distinct
);
EXCEPTION
WHEN OTHERS THEN v_elapsed_ms := FLOOR(
  EXTRACT(
    EPOCH
    FROM (clock_timestamp() - v_start_time)
  ) * 1000
)::bigint;
GET STACKED DIAGNOSTICS v_err_state = RETURNED_SQLSTATE,
v_err_msg = MESSAGE_TEXT,
v_err_detail = PG_EXCEPTION_DETAIL,
v_err_hint = PG_EXCEPTION_HINT,
v_err_context = PG_EXCEPTION_CONTEXT;
INSERT INTO public.reconciliations_logs (
    run_id,
    function_version,
    action,
    target,
    status,
    message,
    company_id,
    transaction_id,
    document_id,
    reconciliation_id,
    rows_affected,
    confidence,
    step_ms,
    processing_ms,
    context
  )
VALUES (
    v_log_run_id,
    v_engine_version,
    'run_error',
    'function',
    'error',
    v_err_msg,
    p_company_id,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    v_elapsed_ms,
    jsonb_build_object(
      'sqlstate',
      v_err_state,
      'detail',
      v_err_detail,
      'hint',
      v_err_hint,
      'context',
      v_err_context
    )
  );
RETURN jsonb_build_object(
  'msg',
  'error',
  'time_ms',
  v_elapsed_ms,
  'transactions_scanned',
  0,
  'documents_considered',
  0,
  'top_per_tx',
  0,
  'top_per_doc',
  0,
  'passed_confidence',
  0,
  'error',
  v_err_msg,
  'sqlstate',
  v_err_state
);
END;
$$;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  -- matching mode
  v_directional_matching boolean := TRUE;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      t.completed_date AS transaction_date_unix,
      d.document_date_unix AS document_date_unix,
      t.merchant_id AS transaction_merchant_id,
      d.merchant_id AS document_merchant_id,

      t.tx_customer_display AS transaction_customer_name,
      d.doc_customer_display AS document_customer_name,

      t.tx_is_in, t.tx_is_out,
      t.tx_merchant_ns, d.doc_merchant_ns,
      t.tx_customer_ns, d.doc_customer_ns,

      t.tx_iban_beneficiary_ns,
      t.tx_iban_counterparty_ns,
      d.doc_iban_ns,
      t.tx_iban_beneficiary_core_ns,
      t.tx_iban_counterparty_core_ns,
      d.doc_iban_core_ns,

      COALESCE(NULLIF(d.doc_dedup_key,''), d.document_id::text) AS doc_dedup_key,

      t.effective_amount AS transaction_effective_amount
    FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope    d ON d.document_id    = p.document_id
    WHERE t.company_id::uuid = d.company_id
  ),

  evaluated AS (
    SELECT
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,

      COALESCE((a.tx_invoice_norm IS NOT NULL AND a.doc_invoice_norm IS NOT NULL
                AND a.tx_invoice_norm = a.doc_invoice_norm), FALSE) AS invoice_number_match,
      COALESCE((a.tx_order_norm   IS NOT NULL AND a.doc_order_norm   IS NOT NULL
                AND a.tx_order_norm = a.doc_order_norm), FALSE)     AS order_number_match,
      COALESCE(a.transaction_currency  = a.document_currency, FALSE) AS currency_match,

      (
        CASE
          WHEN v_directional_matching THEN
            ( a.tx_is_out AND (
                 (a.transaction_merchant_id IS NOT NULL AND a.document_merchant_id IS NOT NULL
                  AND a.transaction_merchant_id = a.document_merchant_id)
              OR (a.tx_merchant_ns IS NOT NULL AND a.doc_merchant_ns IS NOT NULL AND (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 OR position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 OR position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              OR (
                   (a.tx_iban_counterparty_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL AND a.tx_iban_counterparty_ns = a.doc_iban_ns)
                OR (a.tx_iban_counterparty_core_ns IS NOT NULL AND a.doc_iban_core_ns IS NOT NULL AND a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            ))
          ELSE
            (
                 (a.transaction_merchant_id IS NOT NULL AND a.document_merchant_id IS NOT NULL
                  AND a.transaction_merchant_id = a.document_merchant_id)
              OR (a.tx_merchant_ns IS NOT NULL AND a.doc_merchant_ns IS NOT NULL AND (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 OR position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 OR position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              OR (
                   (a.tx_iban_counterparty_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL AND a.tx_iban_counterparty_ns = a.doc_iban_ns)
                OR (a.tx_iban_counterparty_core_ns IS NOT NULL AND a.doc_iban_core_ns IS NOT NULL AND a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            )
        END
      ) AS merchant_match,

      (
        CASE
          WHEN v_directional_matching THEN
            ( a.tx_is_in AND (
                 (a.tx_customer_ns IS NOT NULL AND a.doc_customer_ns IS NOT NULL AND (
                    a.tx_customer_ns = a.doc_customer_ns
                 OR position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 OR position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              OR (
                   (a.tx_iban_beneficiary_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL AND a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                OR (a.tx_iban_beneficiary_core_ns IS NOT NULL AND a.doc_iban_core_ns IS NOT NULL AND a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            ))
          ELSE
            (
                 (a.tx_customer_ns IS NOT NULL AND a.doc_customer_ns IS NOT NULL AND (
                    a.tx_customer_ns = a.doc_customer_ns
                 OR position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 OR position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              OR (
                   (a.tx_iban_beneficiary_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL AND a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                OR (a.tx_iban_beneficiary_core_ns IS NOT NULL AND a.doc_iban_core_ns IS NOT NULL AND a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            )
        END
      ) AS customer_match,

      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, FALSE) AS date_within_cap,

      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND a.document_total = a.transaction_debited)  AS amount_exact_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND a.document_total = a.transaction_credited) AS amount_exact_from_credited,
      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND ABS(a.document_total - a.transaction_debited)  <= a.document_total * v_amount_tolerance_fraction) AS amount_tolerance_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND ABS(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) AS amount_tolerance_from_credited,

      a.tx_is_in, a.tx_is_out,

      -- carry for summary
      a.tx_iban_beneficiary_ns,
      a.tx_iban_counterparty_ns,
      a.doc_iban_ns,
      a.tx_iban_beneficiary_core_ns,
      a.tx_iban_counterparty_core_ns,
      a.doc_iban_core_ns,

      a.tx_merchant_ns,
      a.doc_merchant_ns,
      a.tx_customer_ns,
      a.doc_customer_ns,
      a.transaction_merchant_id,
      a.document_merchant_id
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.*,
      CASE
        WHEN e.tx_is_out AND e.amount_exact_from_debited THEN TRUE
        WHEN e.tx_is_in  AND e.amount_exact_from_credited THEN TRUE
        WHEN NOT v_directional_matching AND (e.amount_exact_from_debited OR e.amount_exact_from_credited) THEN TRUE
        ELSE FALSE
      END AS amount_exact_match,
      CASE
        WHEN v_amount_tolerance_fraction > 0 THEN
          CASE
            WHEN e.tx_is_out AND e.amount_tolerance_from_debited THEN TRUE
            WHEN e.tx_is_in  AND e.amount_tolerance_from_credited THEN TRUE
            WHEN NOT v_directional_matching AND (e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) THEN TRUE
            ELSE FALSE
          END
        ELSE FALSE
      END AS amount_tolerance_match,
      (e.amount_exact_from_debited OR e.amount_exact_from_credited OR e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) AS amount_direction_ok
    FROM evaluated e
  ),

  resolved_weights AS (
    SELECT
      COALESCE((v_weights_json->>'amount_total')::numeric, 0)   AS amount_weight,
      COALESCE((v_weights_json->>'invoice_number')::numeric, 0) AS invoice_weight,
      COALESCE((v_weights_json->>'order_number')::numeric, 0)   AS order_weight,
      COALESCE((v_weights_json->>'date')::numeric, 0)           AS date_weight,
      COALESCE((v_weights_json->>'currency')::numeric, 0)       AS currency_weight,
      COALESCE((v_weights_json->>'merchant_id')::numeric, 0)    AS merchant_weight,
      COALESCE((v_weights_json->>'customer_name')::numeric, 0)  AS customer_weight
  ),

  scored_components AS (
    SELECT
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,

      m.transaction_date_unix,
      m.document_date_unix,
      m.amount_direction_ok,

      m.tx_is_in, m.tx_is_out,

      -- carry fields for summary
      m.tx_iban_beneficiary_ns,
      m.tx_iban_counterparty_ns,
      m.doc_iban_ns,
      m.tx_iban_beneficiary_core_ns,
      m.tx_iban_counterparty_core_ns,
      m.doc_iban_core_ns,

      m.tx_merchant_ns,
      m.doc_merchant_ns,
      m.tx_customer_ns,
      m.doc_customer_ns,
      m.transaction_merchant_id,
      m.document_merchant_id,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN
            w.amount_weight * EXP(
              -0.5 * POWER(
                (ABS(m.transaction_effective_amount - m.document_total))
                / NULLIF((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0)
              , 2)
            )
          ELSE 0
        END, 0
      )::numeric AS amount_score,

      (m.amount_exact_match OR m.amount_tolerance_match) AS amount_match,
      m.invoice_number_match  AS invoice_number_match,
      m.order_number_match    AS order_number_match,

      CASE
        WHEN m.transaction_date_unix IS NOT NULL AND m.document_date_unix IS NOT NULL THEN
          w.date_weight * EXP(
            -0.5 * POWER(
              GREATEST(0,
                (ABS(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days
              ) / NULLIF( GREATEST(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k ), 0)
            , 2)
          )
        ELSE 0
      END::numeric AS date_score,

      m.date_within_cap AS date_match,

      m.currency_match  AS currency_match,
      m.merchant_match  AS merchant_match,
      m.customer_match  AS customer_match,

      w.amount_weight   AS amount_weight,
      w.invoice_weight  AS invoice_weight,
      w.order_weight    AS order_weight,
      w.date_weight     AS date_weight,
      w.currency_weight AS currency_weight,
      w.merchant_weight AS merchant_weight,
      w.customer_weight AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0)  * w.invoice_weight)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)    * w.order_weight)::numeric    AS order_score,
      (COALESCE(m.currency_match::int,0)        * w.currency_weight)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)        * w.merchant_weight)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)        * w.customer_weight)::numeric AS customer_score,

      m.transaction_effective_amount,
      m.document_total
    FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
  ),

  scored_with_applicability AS (
    SELECT
      s.*,

      (
        CASE
          WHEN v_directional_matching THEN
            ( s.tx_is_out AND (
                 (s.doc_merchant_ns IS NOT NULL AND s.tx_merchant_ns IS NOT NULL)
              OR ( (s.doc_iban_ns IS NOT NULL AND s.tx_iban_counterparty_ns IS NOT NULL AND s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   OR (s.doc_iban_core_ns IS NOT NULL AND s.tx_iban_counterparty_core_ns IS NOT NULL AND s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              OR (s.document_merchant_id IS NOT NULL AND s.transaction_merchant_id IS NOT NULL)
            ))
          ELSE
            (
                 (s.doc_merchant_ns IS NOT NULL AND s.tx_merchant_ns IS NOT NULL)
              OR ( (s.doc_iban_ns IS NOT NULL AND s.tx_iban_counterparty_ns IS NOT NULL AND s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   OR (s.doc_iban_core_ns IS NOT NULL AND s.tx_iban_counterparty_core_ns IS NOT NULL AND s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              OR (s.document_merchant_id IS NOT NULL AND s.transaction_merchant_id IS NOT NULL)
            )
        END
      ) AS merchant_applicable,

      (
        CASE
          WHEN v_directional_matching THEN
            ( s.tx_is_in AND (
                 (s.doc_customer_ns IS NOT NULL AND s.tx_customer_ns IS NOT NULL)
              OR ( (s.doc_iban_ns IS NOT NULL AND s.tx_iban_beneficiary_ns IS NOT NULL AND s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   OR (s.doc_iban_core_ns IS NOT NULL AND s.tx_iban_beneficiary_core_ns IS NOT NULL AND s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            ))
          ELSE
            (
                 (s.doc_customer_ns IS NOT NULL AND s.tx_customer_ns IS NOT NULL)
              OR ( (s.doc_iban_ns IS NOT NULL AND s.tx_iban_beneficiary_ns IS NOT NULL AND s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   OR (s.doc_iban_core_ns IS NOT NULL AND s.tx_iban_beneficiary_core_ns IS NOT NULL AND s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            )
        END
      ) AS customer_applicable

    FROM scored_components s
  ),

  scored_with_confidence AS (
    SELECT
      s.*,

      (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (CASE WHEN s.merchant_applicable THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN s.customer_applicable THEN s.customer_weight ELSE 0 END)
      )::numeric AS dynamic_total_weight,

      (
        s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
        + (CASE WHEN s.merchant_applicable THEN s.merchant_score ELSE 0 END)
        + (CASE WHEN s.customer_applicable THEN s.customer_score ELSE 0 END)
      )::numeric AS total_numerator,

      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_number_match::int,0)
     + COALESCE(s.order_number_match::int,0)
     + COALESCE(s.date_match::int,0)
     + COALESCE(s.currency_match::int,0)
     + COALESCE(s.merchant_match::int,0)
     + COALESCE(s.customer_match::int,0)) AS signals_matched_count,

      CASE WHEN (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (CASE WHEN s.merchant_applicable THEN s.merchant_weight ELSE 0 END)
        + (CASE WHEN s.customer_applicable THEN s.customer_weight ELSE 0 END)
      ) > 0 THEN
        ROUND(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
              + (CASE WHEN s.merchant_applicable THEN s.merchant_score ELSE 0 END)
              + (CASE WHEN s.customer_applicable THEN s.customer_score ELSE 0 END))
            / (s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
              + (CASE WHEN s.merchant_applicable THEN s.merchant_weight ELSE 0 END)
              + (CASE WHEN s.customer_applicable THEN s.customer_weight ELSE 0 END))) * 100.0, 4)
      ELSE 0 END AS confidence_percent
    FROM scored_with_applicability s
  ),

  low_signal AS (SELECT * FROM scored_with_confidence WHERE signals_matched_count < 1),
  below_min_confidence AS (
    SELECT * FROM scored_with_confidence
    WHERE signals_matched_count >= 1 AND confidence_percent < v_config_min_confidence
  ),
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE signals_matched_count >= 1 AND confidence_percent >= v_config_min_confidence
  ),

  ranked_per_transaction AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        PARTITION BY b.transaction_id
        ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
      ) AS rownum_per_transaction
    FROM base_candidates b
  ),
  not_top_for_transaction AS (SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction > 1),
  top_per_transaction AS (SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction = 1),

  ranked_per_document AS (
    SELECT
      t.*,
      ROW_NUMBER() OVER (
        PARTITION BY t.doc_dedup_key
        ORDER BY
          t.confidence_percent DESC,
          t.total_numerator DESC,
          t.signals_matched_count DESC,
          ABS(t.transaction_date_unix - t.document_date_unix) ASC,
          t.document_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  not_top_for_document AS (SELECT * FROM ranked_per_document WHERE rownum_per_document > 1),
  top_per_document AS (SELECT * FROM ranked_per_document WHERE rownum_per_document = 1),

  final_candidates AS (
    SELECT *
    FROM top_per_document
    ORDER BY confidence_percent DESC, total_numerator DESC, signals_matched_count DESC, transaction_id, document_id
  ),

  allowed_for_insert AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE  NOT EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       AND NOT EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),
  inserted_reconciliations AS (
    INSERT INTO public.reconciliations (
      transaction_id, document_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary, note
    )
    WITH ts_base AS (
      SELECT f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             COALESCE(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match AS invoice_match,
        d.order_number_match   AS order_match,
        d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,

        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,

        d.amount_score, d.amount_weight,
        d.date_score,   d.date_weight,

        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm, a.tx_order_norm, a.doc_order_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw, a.doc_order_raw,

        a.tx_iban_beneficiary_ns, a.tx_iban_counterparty_ns, a.doc_iban_ns,
        a.tx_iban_beneficiary_core_ns, a.tx_iban_counterparty_core_ns, a.doc_iban_core_ns,

        a.tx_is_in, a.tx_is_out
      FROM ts_base t
      JOIN final_candidates d ON d.transaction_id = t.transaction_id AND d.document_id = t.document_id
      JOIN attributes a       ON a.transaction_id = d.transaction_id AND a.document_id = d.document_id
    ),
    
    notes AS (
      SELECT
        e.*,
        /* % difference between payment and document (2 dp) */
        CASE
          WHEN e.document_total IS NOT NULL AND e.document_total <> 0
          THEN ROUND(ABS(e.transaction_effective_amount - e.document_total) / NULLIF(e.document_total,0) * 100.0, 2)
          ELSE NULL
        END AS amt_diff_pct,

        /* raw time gaps */
        CASE
          WHEN e.transaction_date_unix IS NOT NULL AND e.document_date_unix IS NOT NULL
          THEN ABS(e.transaction_date_unix - e.document_date_unix)
          ELSE NULL
        END AS sec_diff,

        /* derived gaps */
        CASE WHEN e.transaction_date_unix IS NOT NULL AND e.document_date_unix IS NOT NULL
          THEN ABS(e.transaction_date_unix - e.document_date_unix) / 86400.0
          ELSE NULL END AS day_gap,
        CASE WHEN e.transaction_date_unix IS NOT NULL AND e.document_date_unix IS NOT NULL
          THEN ABS(e.transaction_date_unix - e.document_date_unix) / 3600.0
          ELSE NULL END AS hour_gap,
        CASE WHEN e.transaction_date_unix IS NOT NULL AND e.document_date_unix IS NOT NULL
          THEN ABS(e.transaction_date_unix - e.document_date_unix) / 60.0
          ELSE NULL END AS minute_gap,

        /* ratios shown in notes: amount 2 dp; date integer % */
        CASE WHEN e.amount_weight > 0 THEN ROUND((e.amount_score / e.amount_weight) * 100.0, 2) END AS amount_ratio_pct,
        CASE WHEN e.date_weight   > 0 THEN ROUND((e.date_score   / e.date_weight)   * 100.0, 0)  END AS date_ratio_pct
      FROM enrich e
    ),

    notes_text AS (
      SELECT
        n.*,  
        CASE
          WHEN n.date_weight > 0 AND n.date_ratio_pct <= 99 THEN
            TRIM(BOTH ' ' FROM CONCAT_WS(' ',
              /* amount note shown only if amount was penalized */
              CASE
                WHEN n.amount_weight > 0 AND n.amount_score < n.amount_weight AND n.amt_diff_pct IS NOT NULL
                THEN CONCAT('Amount scored ', COALESCE(n.amount_ratio_pct::text,'0'),
                            '% because payment vs document differed by ', n.amt_diff_pct::text, '%.')
              END,
              /* date penalized: round days to 2 dp */
              CASE
                WHEN n.day_gap IS NOT NULL
                THEN CONCAT('Date scored ', COALESCE(n.date_ratio_pct::text,'0'),
                            '% because dates were ', ROUND(n.day_gap::numeric, 2)::text, ' days apart.')
              END
            ))
          ELSE NULL
        END AS note_text
      FROM notes n
    )
    SELECT
      e.transaction_id, e.document_id, e.document_company_id,
      COALESCE(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      EXTRACT(YEAR FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          SELECT json_agg(s) FROM unnest(ARRAY[
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]) s WHERE s IS NOT NULL
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  COALESCE(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_order',     e.tx_order_norm,
        'doc_order',    COALESCE(e.doc_order_raw, e.doc_order_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_iban_beneficiary',        e.tx_iban_beneficiary_ns,
        'tx_iban_counterparty',       e.tx_iban_counterparty_ns,
        'doc_iban',                   e.doc_iban_ns,
        'tx_iban_beneficiary_core',   e.tx_iban_beneficiary_core_ns,
        'tx_iban_counterparty_core',  e.tx_iban_counterparty_core_ns,
        'doc_iban_core',              e.doc_iban_core_ns,
        'tx_is_in',    e.tx_is_in,
        'tx_is_out',   e.tx_is_out,
        'direction',   CASE WHEN e.tx_is_in THEN 'incoming' WHEN e.tx_is_out THEN 'outgoing' ELSE 'unknown' END,
        'days_between_tx_and_doc',
          CASE
            WHEN e.transaction_date_unix IS NOT NULL AND e.document_date_unix IS NOT NULL
              THEN (ABS(e.transaction_date_unix - e.document_date_unix) / 86400)::int
            ELSE NULL
          END
      )::text,
      NULLIF(nt.note_text,'')  -- note
    FROM notes_text nt
    JOIN enrich e USING (transaction_id, document_id)
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score,  invoice_score,  order_score,  merchant_score,  amount_score,  currency_score,  customer_score
    )
    SELECT
      r.reconciliation_id,
      d.document_company_id AS company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      d.order_number_match,
      d.merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      d.order_weight,
      d.merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      d.order_score,
      d.merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    FROM inserted_reconciliations r
    JOIN scored_with_confidence d
      ON d.transaction_id = r.transaction_id
     AND d.document_id    = r.document_id
    RETURNING 1
  ),

  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM top_per_transaction)      AS top_per_transaction_count,
      (SELECT COUNT(*) FROM top_per_document)         AS top_per_document_count,
      (SELECT COUNT(*) FROM final_candidates)         AS passed_confidence_count,
      (SELECT COUNT(*) FROM inserted_reconciliations) AS inserted_count,
      (SELECT COUNT(*) FROM base_candidates)          AS candidates_count,
      (SELECT COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb) FROM seed_counts_total)    AS seed_counts_total_json,
      (SELECT COALESCE(jsonb_object_agg(seed, ct), '{}'::jsonb) FROM seed_counts_distinct) AS seed_counts_distinct_json,
      (SELECT COUNT(*) FROM transactions_scope)       AS total_tx_count,
      (SELECT COUNT(*) FROM document_scope)           AS total_doc_count
  )
  SELECT
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  INTO
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  FROM final_counts;

  -- end log
  v_elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - v_start_time)) * 1000)::bigint;

  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    v_inserted_count, NULL, NULL, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered',  v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic',  v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  RETURN jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered',  v_total_docs,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic',  v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

EXCEPTION WHEN OTHERS THEN
  v_elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - v_start_time)) * 1000)::bigint;

  GET STACKED DIAGNOSTICS
    v_err_state   = RETURNED_SQLSTATE,
    v_err_msg     = MESSAGE_TEXT,
    v_err_detail  = PG_EXCEPTION_DETAIL,
    v_err_hint    = PG_EXCEPTION_HINT,
    v_err_context = PG_EXCEPTION_CONTEXT;

  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, v_elapsed_ms,
    jsonb_build_object(
      'sqlstate', v_err_state,
      'detail',   v_err_detail,
      'hint',     v_err_hint,
      'context',  v_err_context
    )
  );

  RETURN jsonb_build_object(
    'msg','error','time_ms',v_elapsed_ms,
    'transactions_scanned',0,'documents_considered',0,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',v_err_msg,'sqlstate',v_err_state
  );
END;
$$;

alter function athena_reconcile_general_v1_9_n(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v1_9_n(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v1_9_n(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v1_9_n(unknown, unknown) to service_role;

create or replace function athena_reconcile_general_invoices_v2(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
declare
  v_status_message text := 'success';
  v_engine_version text := '1.9M-invoices';
  v_start_time timestamptz := clock_timestamp();
  v_elapsed_ms bigint := 0;
  v_config_amount_tolerance numeric;
  v_config_date_window_days numeric;
  v_config_min_confidence numeric;
  v_config_allow_document_duplicates boolean;
  v_config_log_activity boolean;
  v_amount_tolerance_fraction numeric;
  v_date_window_seconds bigint;
  v_date_full_weight_grace_days int := 90;
  v_edge_weight_small constant numeric := 0.10;
  v_gauss_k constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small));
  v_cap_precise int := 5;
  v_cap_broad int := 10;
  v_cap_window_only int := 3;
  v_max_transactions_to_scan int := 20000;
  v_weights_json jsonb := '{}'::jsonb;
  v_weights_total_possible numeric := 0;
  v_summary_top_per_tx bigint := 0;
  v_summary_top_per_doc bigint := 0;
  v_summary_passed_count bigint := 0;
  v_inserted_count bigint := 0;
  v_candidates_count bigint := 0;
  v_total_tx bigint := 0;
  v_total_docs bigint := 0;
  v_seed_counts_total jsonb := '{}'::jsonb;
  v_seed_counts_distinct jsonb := '{}'::jsonb;
  v_log_run_id uuid := gen_random_uuid();
  v_log_run_unix_time bigint := extract(epoch from now())::bigint;
  v_directional_matching boolean := true;
  v_err_state text;
  v_err_msg text;
  v_err_detail text;
  v_err_hint text;
  v_err_context text;
begin
  select amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  into   v_config_amount_tolerance, v_config_date_window_days, v_config_min_confidence,
         v_config_allow_document_duplicates, v_config_log_activity
  from   public.reconciliations_config
  where  reconciliation_config_id = p_reconciliation_config_id;

  if not found then
    v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
    return jsonb_build_object('msg','reconciliations_config not found','time_ms',v_elapsed_ms,
                              'transactions_scanned',0,'documents_considered',0,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  end if;

  v_amount_tolerance_fraction := case when v_config_amount_tolerance >= 1 then v_config_amount_tolerance / 100.0 else v_config_amount_tolerance end;
  v_date_window_seconds := ceil(coalesce(v_config_date_window_days, 1) * 86400)::bigint;
  if v_config_min_confidence is null then v_config_min_confidence := 0; end if;
  if v_config_min_confidence <= 1 then v_config_min_confidence := v_config_min_confidence * 100.0; end if;

  with candidate_weights as (
    select lower(trim(w.weight_key)) as k_norm, (w.weight_value)::numeric as v_norm, w.company_id
    from public.reconciliations_weight_registry w
    where w.active
      and w.merchant_id is null
      and ((p_company_id is null and w.company_id is null) or (p_company_id is not null and (w.company_id = p_company_id or w.company_id is null)))
      and lower(trim(w.weight_key)) in ('amount_total','invoice_number','date','currency','customer_name')
  ),
  picked as (
    select distinct on (k_norm) k_norm, v_norm
    from candidate_weights
    order by k_norm, (company_id is null)
  )
  select coalesce(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) into v_weights_json from picked;

  select coalesce((v_weights_json->>'amount_total')::numeric,0)
       + coalesce((v_weights_json->>'invoice_number')::numeric,0)
       + coalesce((v_weights_json->>'date')::numeric,0)
       + coalesce((v_weights_json->>'currency')::numeric,0)
       + coalesce((v_weights_json->>'customer_name')::numeric,0)
  into v_weights_total_possible;

  if v_weights_total_possible = 0 then
    v_weights_json := jsonb_build_object('amount_total',1,'invoice_number',1,'date',1,'currency',1,'customer_name',1);
    v_weights_total_possible := 5;
  end if;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  ) values (
    v_log_run_id, v_engine_version, 'run_start', 'function', 'ok', null,
    p_company_id, null, null, null,
    null, null, null, null,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', v_config_log_activity,
        'allow_doc_dups', v_config_allow_document_duplicates,
        'amount_tol_pct', v_amount_tolerance_fraction,
        'min_confidence', v_config_min_confidence,
        'date_window_days', v_config_date_window_days,
        'max_tx_seed', v_max_transactions_to_scan,
        'cap_precise', v_cap_precise,
        'cap_broad',   v_cap_broad,
        'cap_window_only', v_cap_window_only,
        'directional_matching', v_directional_matching,
        'gauss_edge_weight', v_edge_weight_small
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  with
  invoice_scope as (
    select
      i.invoice_id as document_id,
      i.author_company_id as company_id_text,
      coalesce(case when i.author_company_id ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' then i.author_company_id::uuid end, p_company_id) as company_id_uuid,
      i.total as total,
      i.currency as currency,
      nullif(i.invoice_nr,'') as invoice_number_raw,
      nullif(public.norm_code(i.invoice_nr),'') as invoice_number_norm,
      case when i.issue_date is not null then i.issue_date::bigint when i.due_date is not null then i.due_date::bigint else null end as document_date_unix,
      nullif(public.norm_text_ns(concat_ws(' ', i.recipient_name, i.recipient_company, i.recipient_first_name, i.recipient_last_name, i.recipient_email, i.recipient_phone)), '') as doc_customer_ns,
      coalesce(i.recipient_name, i.recipient_company, concat_ws(' ', i.recipient_first_name, i.recipient_last_name), i.recipient_email) as doc_customer_display,
      coalesce(i.invoice_id::text, i.id::text) as doc_dedup_key
    from public.invoices i
    where (p_company_id is null or i.author_company_id = p_company_id::text)
      and i.reconciliation_id is null
  ),

  transactions_scope as (
    select
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      nullif(public.norm_code(coalesce(nullif(t.invoice_nr,''), (select m[1] from regexp_matches(upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)), '(INV(?:OICE)?[ -_/]?[A-Z0-9._-]{1,24}|FACTUUR[ -_/]?[A-Z0-9._-]{1,24}|[0-9]{5,10}[-/][A-Z0-9]{1,10})', 'g') as m limit 1))), '') as invoice_nr_norm,
      public.norm_code(upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description))) as tx_text_code,
      t.completed_date,
      (nullif(t.credited,0) is not null or nullif(t.money_in,0)  is not null) as tx_is_in,
      (nullif(t.debited,0)  is not null or nullif(t.money_out,0) is not null) as tx_is_out,
      nullif(public.norm_text_ns(coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty)), '') as tx_customer_ns,
      coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty) as tx_customer_display,
      case when t.debited is not null and t.debited > 0 then t.debited when t.credited is not null and t.credited > 0 then t.credited when t.amount is not null then abs(t.amount) else null end as effective_amount
    from public.transactions t
    where (p_company_id is null or t.company_id = p_company_id::text)
      and t.reconciled is not true
      and (t.invoice_nr is not null
        or (t.currency is not null and (t.name_consumer is not null or t.account_holder_name is not null or t.account_holder_name_counterparty is not null))
        or (t.currency is not null and (t.amount is not null or t.debited is not null or t.credited is not null)))
    order by t.completed_date desc, t.transaction_id
    limit v_max_transactions_to_scan
  ),

  s1_invoice as (
    select t.transaction_id, d.document_id, 'invoice_number' as seed
    from transactions_scope t
    join invoice_scope d
      on d.company_id_text = t.company_id
     and d.currency = t.currency
     and d.invoice_number_norm is not null
     and ((t.invoice_nr_norm is not null and d.invoice_number_norm = t.invoice_nr_norm)
       or (t.tx_text_code is not null and position(d.invoice_number_norm in t.tx_text_code) > 0))
  ),

  s2_customer_exact_amount as (
    select t.transaction_id, d_sub.document_id, 'customer_ns_exact_amount' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and ((not v_directional_matching) or t.tx_is_in)
        and t.credited is not null and t.credited > 0
        and d.total = t.credited
        and d.doc_customer_ns is not null and t.tx_customer_ns is not null
        and (d.doc_customer_ns = t.tx_customer_ns or position(d.doc_customer_ns in t.tx_customer_ns) > 0 or position(t.tx_customer_ns in d.doc_customer_ns) > 0)
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
      order by d.document_date_unix, d.document_id
      limit v_cap_precise
    ) d_sub on true
    where done.transaction_id is null
  ),

  s3_window_amount_exact as (
    select t.transaction_id, d_sub.document_id, 'window_amount_exact' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
        and (((not v_directional_matching) or t.tx_is_in) and t.credited is not null and t.credited > 0 and d.total = t.credited
          or ((not v_directional_matching) or t.tx_is_out) and t.debited is not null and t.debited > 0 and d.total = t.debited)
      order by d.document_date_unix, d.document_id
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s4_window_amount_tol as (
    select t.transaction_id, d_sub.document_id, 'window_amount_tol' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount union all select distinct transaction_id from s3_window_amount_exact) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
        and v_amount_tolerance_fraction > 0
        and (((not v_directional_matching) or t.tx_is_in) and t.credited is not null and t.credited > 0 and d.total between (t.credited / (1 + v_amount_tolerance_fraction)) and (t.credited / (1 - v_amount_tolerance_fraction))
          or ((not v_directional_matching) or t.tx_is_out) and t.debited is not null and t.debited > 0 and d.total between (t.debited / (1 + v_amount_tolerance_fraction)) and (t.debited / (1 - v_amount_tolerance_fraction)))
      order by d.document_date_unix, d.document_id
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s5_window_only as (
    select t.transaction_id, d_sub.document_id, 'window_only' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount union all select distinct transaction_id from s3_window_amount_exact union all select distinct transaction_id from s4_window_amount_tol) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
      order by d.document_date_unix, d.document_id
      limit v_cap_window_only
    ) d_sub on true
    where done.transaction_id is null
  ),

  seeds as (
    select * from s1_invoice
    union all select * from s2_customer_exact_amount
    union all select * from s3_window_amount_exact
    union all select * from s4_window_amount_tol
    union all select * from s5_window_only
  ),

  seed_counts_total as (select seed, count(*) as ct from seeds group by seed),
  seed_counts_distinct as (select seed, count(distinct (transaction_id, document_id)) as ct from seeds group by seed),

  candidate_pairs as (select distinct transaction_id, document_id from seeds),

  distinct_pairs as (select distinct transaction_id, document_id from candidate_pairs),

  attributes as (
    select
      p.transaction_id,
      p.document_id,
      d.company_id_uuid as document_company_id,
      t.amount as transaction_amount,
      t.debited as transaction_debited,
      t.credited as transaction_credited,
      d.total as document_total,
      t.currency as transaction_currency,
      d.currency as document_currency,
      t.invoice_nr_norm as tx_invoice_norm,
      d.invoice_number_norm as doc_invoice_norm,
      d.invoice_number_raw as doc_invoice_raw,
      t.completed_date as transaction_date_unix,
      d.document_date_unix as document_date_unix,
      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_customer_display as transaction_customer_name,
      d.doc_customer_display as document_customer_name,
      t.tx_text_code,
      coalesce(nullif(d.doc_dedup_key,''), d.document_id::text) as doc_dedup_key,
      t.effective_amount as transaction_effective_amount,
      t.tx_is_in,
      t.tx_is_out
    from distinct_pairs p
    join transactions_scope t on t.transaction_id = p.transaction_id
    join invoice_scope    d on d.document_id    = p.document_id
    where t.company_id = d.company_id_text
  ),

  evaluated as (
    select
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,
      coalesce(((a.tx_invoice_norm is not null and a.doc_invoice_norm is not null and a.tx_invoice_norm = a.doc_invoice_norm) or (a.tx_text_code is not null and a.doc_invoice_norm is not null and position(a.doc_invoice_norm in a.tx_text_code) > 0)), false) as invoice_number_match,
      coalesce(a.transaction_currency  = a.document_currency, false) as currency_match,
      (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (a.tx_customer_ns = a.doc_customer_ns or position(a.doc_customer_ns in a.tx_customer_ns) > 0 or position(a.tx_customer_ns in a.doc_customer_ns) > 0)) as customer_match,
      coalesce(abs(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, false) as date_within_cap,
      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,
      (a.transaction_debited is not null and a.transaction_debited > 0 and a.document_total = a.transaction_debited) as amount_exact_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and a.document_total = a.transaction_credited) as amount_exact_from_credited,
      (a.transaction_debited is not null and a.transaction_debited > 0 and abs(a.document_total - a.transaction_debited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and abs(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_credited,
      a.tx_is_in,
      a.tx_is_out,
      (a.transaction_credited is not null and a.transaction_credited > 0) as credited_pos,
      (a.transaction_debited is not null and a.transaction_debited > 0) as debited_pos
    from attributes a
  ),

  with_amount_tolerance as (
    select
      e.*,
      case when v_directional_matching then (e.tx_is_out and e.amount_exact_from_debited) or (e.tx_is_in and e.amount_exact_from_credited) else (e.amount_exact_from_debited or e.amount_exact_from_credited) end as amount_exact_match,
      case when v_amount_tolerance_fraction > 0 then case when v_directional_matching then (e.tx_is_out and e.amount_tolerance_from_debited) or (e.tx_is_in and e.amount_tolerance_from_credited) else (e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) end else false end as amount_tolerance_match
    from evaluated e
  ),

  resolved_weights as (
    select
      coalesce((v_weights_json->>'amount_total')::numeric, 0) as amount_weight,
      coalesce((v_weights_json->>'invoice_number')::numeric, 0) as invoice_weight,
      coalesce((v_weights_json->>'date')::numeric, 0) as date_weight,
      coalesce((v_weights_json->>'currency')::numeric, 0) as currency_weight,
      coalesce((v_weights_json->>'customer_name')::numeric, 0) as customer_weight
  ),

  scored_components as (
    select
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,
      m.transaction_date_unix,
      m.document_date_unix,
      m.tx_is_in, m.tx_is_out,
      coalesce(case when m.amount_exact_match then w.amount_weight when m.amount_tolerance_match then w.amount_weight * exp(-0.5 * power((abs(m.transaction_effective_amount - m.document_total)) / nullif((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0), 2)) else 0 end, 0)::numeric as amount_score,
      (m.amount_exact_match or m.amount_tolerance_match) as amount_match,
      m.invoice_number_match as invoice_number_match,
      case when m.transaction_date_unix is not null and m.document_date_unix is not null then w.date_weight * exp(-0.5 * power(greatest(0, (abs(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days) / nullif(greatest(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k), 0), 2)) else 0 end::numeric as date_score,
      m.date_within_cap as date_match,
      m.currency_match as currency_match,
      m.customer_match as customer_match,
      w.amount_weight as amount_weight,
      w.invoice_weight as invoice_weight,
      w.date_weight as date_weight,
      w.currency_weight as currency_weight,
      w.customer_weight as customer_weight,
      (coalesce(m.invoice_number_match::int,0) * w.invoice_weight)::numeric as invoice_score,
      (coalesce(m.currency_match::int,0) * w.currency_weight)::numeric as currency_score,
      (coalesce(m.customer_match::int,0) * w.customer_weight)::numeric as customer_score,
      m.transaction_effective_amount,
      m.document_total
    from with_amount_tolerance m
    cross join resolved_weights w
  ),

  scored_with_confidence as (
    select
      s.*,
      (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight)::numeric as dynamic_total_weight,
      (s.amount_score + s.invoice_score + s.date_score + s.currency_score + s.customer_score)::numeric as total_numerator,
      (coalesce(s.amount_match::int,0) + coalesce(s.invoice_number_match::int,0) + coalesce(s.date_match::int,0) + coalesce(s.currency_match::int,0) + coalesce(s.customer_match::int,0)) as signals_matched_count,
      case when (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight) > 0 then round(((s.amount_score + s.invoice_score + s.date_score + s.currency_score + s.customer_score) / (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight)) * 100.0, 4) else 0 end as confidence_percent
    from scored_components s
  ),

  low_signal as (select * from scored_with_confidence where signals_matched_count < 1),
  below_min_confidence as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent < v_config_min_confidence),
  base_candidates as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent >= v_config_min_confidence),

  ranked_per_transaction as (
    select b.*, row_number() over (partition by b.transaction_id order by b.confidence_percent desc, b.total_numerator desc, b.signals_matched_count desc, b.document_id) as rownum_per_transaction
    from base_candidates b
  ),
  not_top_for_transaction as (select * from ranked_per_transaction where rownum_per_transaction > 1),
  top_per_transaction as (select * from ranked_per_transaction where rownum_per_transaction = 1),

  ranked_per_document as (
    select t.*, row_number() over (partition by t.doc_dedup_key order by t.confidence_percent desc, t.total_numerator desc, t.signals_matched_count desc, abs(t.transaction_date_unix - t.document_date_unix) asc, t.document_id) as rownum_per_document
    from top_per_transaction t
  ),
  not_top_for_document as (select * from ranked_per_document where rownum_per_document > 1),
  top_per_document as (select * from ranked_per_document where rownum_per_document = 1),

  final_candidates as (
    select * from top_per_document
    order by confidence_percent desc, total_numerator desc, signals_matched_count desc, transaction_id, document_id
  ),

  allowed_for_insert as (
    select f.*
    from   final_candidates f
    where  not exists (select 1 from public.reconciliations r1 where r1.transaction_id = f.transaction_id)
       and not exists (select 1 from public.reconciliations r2 where r2.invoice_id    = f.document_id)
  ),

  inserted_reconciliations as (
    insert into public.reconciliations (
      transaction_id, invoice_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary, note
    )
    with ts_base as (
      select f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             coalesce(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint as base_unix
      from allowed_for_insert f
    ),
    enrich as (
      select
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match as invoice_match,
        d.amount_match, d.date_match, d.currency_match, d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        d.amount_score, d.amount_weight,
        d.date_score,   d.date_weight,
        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw,
        (a.tx_is_in or e.credited_pos) as tx_is_in_final,
        (a.tx_is_out or e.debited_pos) as tx_is_out_final
      from ts_base t
      join final_candidates d on d.transaction_id = t.transaction_id and d.document_id = t.document_id
      join evaluated        e on e.transaction_id = d.transaction_id and e.document_id = d.document_id
      join attributes       a on a.transaction_id = d.transaction_id and a.document_id = d.document_id
    ),
    notes as (
      select
        e.*,
        case when e.document_total is not null and e.document_total <> 0 then round(abs(e.transaction_effective_amount - e.document_total) / nullif(e.document_total,0) * 100.0, 2) else null end as amt_diff_pct,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) else null end as sec_diff,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) / 86400.0 else null end as day_gap,
        case when e.amount_weight > 0 then round((e.amount_score / e.amount_weight) * 100.0, 2) end as amount_ratio_pct,
        case when e.date_weight   > 0 then round((e.date_score   / e.date_weight)   * 100.0, 0)  end as date_ratio_pct
      from enrich e
    ),
    notes_text as (
      select
        n.*,
        case
          when n.date_weight > 0 and n.date_ratio_pct <= 99 then
            trim(both ' ' from concat_ws(' ',
              case when n.amount_weight > 0 and n.amount_score < n.amount_weight and n.amt_diff_pct is not null then concat('Amount scored ', coalesce(n.amount_ratio_pct::text,'0'), '% because payment vs invoice differed by ', n.amt_diff_pct::text, '%.') end,
              case when n.day_gap is not null then concat('Date scored ', coalesce(n.date_ratio_pct::text,'0'), '% because dates were ', round(n.day_gap::numeric, 2)::text, ' days apart.') end
            ))
          else null
        end as note_text
      from notes n
    )
    select
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      coalesce(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      extract(year from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      extract(quarter from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) at time zone 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          select json_agg(s) from unnest(array[
            case when e.invoice_match  then 'invoice'  end,
            case when e.amount_match   then 'amount'   end,
            case when e.date_match     then 'date'     end,
            case when e.currency_match then 'currency' end,
            case when e.customer_match then 'customer' end
          ]) s where s is not null
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  coalesce(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_is_in',     e.tx_is_in_final,
        'tx_is_out',    e.tx_is_out_final,
        'direction',    case when e.tx_is_in_final then 'incoming' when e.tx_is_out_final then 'outgoing' else 'unknown' end,
        'days_between_tx_and_doc',
          case when e.transaction_date_unix is not null and e.document_date_unix is not null then (abs(e.transaction_date_unix - e.document_date_unix) / 86400)::int else null end
      )::text,
      nullif(nt.note_text,'')
    from notes_text nt
    join enrich e using (transaction_id, document_id)
    returning reconciliation_id, transaction_id, invoice_id, confidence
  ),

  inserted_match_scores as (
    insert into public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score,  invoice_score,  order_score,  merchant_score,  amount_score,  currency_score,  customer_score
    )
    select
      r.reconciliation_id,
      d.document_company_id as company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      false as order_match,
      false as merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      0 as order_weight,
      0 as merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      0 as order_score,
      0 as merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    from inserted_reconciliations r
    join scored_with_confidence d
      on d.transaction_id = r.transaction_id
     and d.document_id    = r.invoice_id
    returning 1
  ),

  final_counts as (
    select
      (select count(*) from top_per_transaction) as top_per_transaction_count,
      (select count(*) from top_per_document) as top_per_document_count,
      (select count(*) from final_candidates) as passed_confidence_count,
      (select count(*) from inserted_reconciliations) as inserted_count,
      (select count(*) from base_candidates) as candidates_count,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_total) as seed_counts_total_json,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_distinct) as seed_counts_distinct_json,
      (select count(*) from transactions_scope) as total_tx_count,
      (select count(*) from invoice_scope) as total_doc_count
  )
  select
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  into
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  from final_counts;

  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', null,
    p_company_id, null, null, null,
    v_inserted_count, null, null, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered',  v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic',  v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  return jsonb_build_object(
    'msg', v_status_message,
    'time_ms', v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered',  v_total_docs,
    'top_per_tx', v_summary_top_per_tx,
    'top_per_doc', v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic',  v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

exception when others then
  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
  get stacked diagnostics
    v_err_state   = returned_sqlstate,
    v_err_msg     = message_text,
    v_err_detail  = pg_exception_detail,
    v_err_hint    = pg_exception_hint,
    v_err_context = pg_exception_context;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg,
    p_company_id, null, null, null,
    null, null, null, v_elapsed_ms,
    jsonb_build_object('sqlstate', v_err_state, 'detail', v_err_detail, 'hint', v_err_hint, 'context', v_err_context)
  );

  return jsonb_build_object(
    'msg','error','time_ms',v_elapsed_ms,
    'transactions_scanned',0,'documents_considered',0,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',v_err_msg,'sqlstate',v_err_state
  );
end;
$$;

alter function athena_reconcile_general_invoices_v2(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_invoices_v2(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_invoices_v2(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_invoices_v2(unknown, unknown) to service_role;

create or replace function athena_reconcile_general_invoices_latest(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
declare
  v_status_message text := 'success';
  v_engine_version text := '1.9M-invoices';
  v_start_time timestamptz := clock_timestamp();
  v_elapsed_ms bigint := 0;
  v_config_amount_tolerance numeric;
  v_config_date_window_days numeric;
  v_config_min_confidence numeric;
  v_config_allow_document_duplicates boolean;
  v_config_log_activity boolean;
  v_amount_tolerance_fraction numeric;
  v_date_window_seconds bigint;
  v_date_full_weight_grace_days int := 30;
  v_edge_weight_small constant numeric := 0.10;
  v_gauss_k constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small));
  v_cap_precise int := 5;
  v_cap_broad int := 10;
  v_cap_window_only int := 3;
  v_max_transactions_to_scan int := 20000;
  v_weights_json jsonb := '{}'::jsonb;
  v_weights_total_possible numeric := 0;
  v_summary_top_per_tx bigint := 0;
  v_summary_top_per_doc bigint := 0;
  v_summary_passed_count bigint := 0;
  v_inserted_count bigint := 0;
  v_candidates_count bigint := 0;
  v_total_tx bigint := 0;
  v_total_docs bigint := 0;
  v_seed_counts_total jsonb := '{}'::jsonb;
  v_seed_counts_distinct jsonb := '{}'::jsonb;
  v_log_run_id uuid := gen_random_uuid();
  v_log_run_unix_time bigint := extract(epoch from now())::bigint;
  v_directional_matching boolean := true;
  v_err_state text;
  v_err_msg text;
  v_err_detail text;
  v_err_hint text;
  v_err_context text;
begin
  select amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  into   v_config_amount_tolerance, v_config_date_window_days, v_config_min_confidence,
         v_config_allow_document_duplicates, v_config_log_activity
  from   public.reconciliations_config
  where  reconciliation_config_id = p_reconciliation_config_id;

  if not found then
    v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
    return jsonb_build_object('msg','reconciliations_config not found','time_ms',v_elapsed_ms,
                              'transactions_scanned',0,'documents_considered',0,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  end if;

  v_amount_tolerance_fraction := case when v_config_amount_tolerance >= 1 then v_config_amount_tolerance / 100.0 else v_config_amount_tolerance end;
  v_date_window_seconds := ceil(coalesce(v_config_date_window_days, 1) * 86400)::bigint;
  if v_config_min_confidence is null then v_config_min_confidence := 0; end if;
  if v_config_min_confidence <= 1 then v_config_min_confidence := v_config_min_confidence * 100.0; end if;

  with candidate_weights as (
    select lower(trim(w.weight_key)) as k_norm, (w.weight_value)::numeric as v_norm, w.company_id
    from public.reconciliations_weight_registry w
    where w.active
      and w.merchant_id is null
      and ((p_company_id is null and w.company_id is null) or (p_company_id is not null and (w.company_id = p_company_id or w.company_id is null)))
      and lower(trim(w.weight_key)) in ('amount_total','invoice_number','date','currency','customer_name')
  ),
  picked as (
    select distinct on (k_norm) k_norm, v_norm
    from candidate_weights
    order by k_norm, (company_id is null)
  )
  select coalesce(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) into v_weights_json from picked;

  select coalesce((v_weights_json->>'amount_total')::numeric,0)
       + coalesce((v_weights_json->>'invoice_number')::numeric,0)
       + coalesce((v_weights_json->>'date')::numeric,0)
       + coalesce((v_weights_json->>'currency')::numeric,0)
       + coalesce((v_weights_json->>'customer_name')::numeric,0)
  into v_weights_total_possible;

  if v_weights_total_possible = 0 then
    v_weights_json := jsonb_build_object('amount_total',1,'invoice_number',1,'date',1,'currency',1,'customer_name',1);
    v_weights_total_possible := 5;
  end if;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  ) values (
    v_log_run_id, v_engine_version, 'run_start', 'function', 'ok', null,
    p_company_id, null, null, null,
    null, null, null, null,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', v_config_log_activity,
        'allow_doc_dups', v_config_allow_document_duplicates,
        'amount_tol_pct', v_amount_tolerance_fraction,
        'min_confidence', v_config_min_confidence,
        'date_window_days', v_config_date_window_days,
        'max_tx_seed', v_max_transactions_to_scan,
        'cap_precise', v_cap_precise,
        'cap_broad',   v_cap_broad,
        'cap_window_only', v_cap_window_only,
        'directional_matching', v_directional_matching,
        'gauss_edge_weight', v_edge_weight_small
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  with
  invoice_scope as (
    select
      i.invoice_id as document_id,
      i.author_company_id as company_id_text,
      coalesce(case when i.author_company_id ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' then i.author_company_id::uuid end, p_company_id) as company_id_uuid,
      i.total as total,
      i.currency as currency,
      nullif(i.invoice_nr,'') as invoice_number_raw,
      nullif(public.norm_code(i.invoice_nr),'') as invoice_number_norm,
      case when i.issue_date is not null then i.issue_date::bigint when i.due_date is not null then i.due_date::bigint else null end as document_date_unix,
      nullif(public.norm_text_ns(concat_ws(' ', i.recipient_name, i.recipient_company, i.recipient_first_name, i.recipient_last_name, i.recipient_email, i.recipient_phone)), '') as doc_customer_ns,
      coalesce(i.recipient_name, i.recipient_company, concat_ws(' ', i.recipient_first_name, i.recipient_last_name), i.recipient_email) as doc_customer_display,
      coalesce(i.invoice_id::text, i.id::text) as doc_dedup_key
    from public.invoices i
    where (p_company_id is null or i.author_company_id = p_company_id::text)
      and i.reconciliation_id is null
  ),

  transactions_scope as (
    select
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      nullif(public.norm_code(coalesce(nullif(t.invoice_nr,''), (select m[1] from regexp_matches(upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)), '(INV(?:OICE)?[ -_/]?[A-Z0-9._-]{1,24}|FACTUUR[ -_/]?[A-Z0-9._-]{1,24}|[0-9]{5,10}[-/][A-Z0-9]{1,10})', 'g') as m limit 1))), '') as invoice_nr_norm,
      public.norm_code(upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description))) as tx_text_code,
      t.completed_date,
      (nullif(t.credited,0) is not null or nullif(t.money_in,0)  is not null) as tx_is_in,
      (nullif(t.debited,0)  is not null or nullif(t.money_out,0) is not null) as tx_is_out,
      nullif(public.norm_text_ns(coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty)), '') as tx_customer_ns,
      coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty) as tx_customer_display,
      case when t.debited is not null and t.debited > 0 then t.debited when t.credited is not null and t.credited > 0 then t.credited when t.amount is not null then abs(t.amount) else null end as effective_amount
    from public.transactions t
    where (p_company_id is null or t.company_id = p_company_id::text)
      and t.reconciled is not true
      and (t.invoice_nr is not null
        or (t.currency is not null and (t.name_consumer is not null or t.account_holder_name is not null or t.account_holder_name_counterparty is not null))
        or (t.currency is not null and (t.amount is not null or t.debited is not null or t.credited is not null)))
    order by t.completed_date desc, t.transaction_id
    limit v_max_transactions_to_scan
  ),

  s1_invoice as (
    select t.transaction_id, d.document_id, 'invoice_number' as seed
    from transactions_scope t
    join invoice_scope d
      on d.company_id_text = t.company_id
     and d.currency = t.currency
     and d.invoice_number_norm is not null
     and ((t.invoice_nr_norm is not null and d.invoice_number_norm = t.invoice_nr_norm)
       or (t.tx_text_code is not null and position(d.invoice_number_norm in t.tx_text_code) > 0))
  ),

  s2_customer_exact_amount as (
    select t.transaction_id, d_sub.document_id, 'customer_ns_exact_amount' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and ((not v_directional_matching) or t.tx_is_in)
        and t.credited is not null and t.credited > 0
        and d.total = t.credited
        and d.doc_customer_ns is not null and t.tx_customer_ns is not null
        and (d.doc_customer_ns = t.tx_customer_ns or position(d.doc_customer_ns in t.tx_customer_ns) > 0 or position(t.tx_customer_ns in d.doc_customer_ns) > 0)
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
      order by d.document_date_unix, d.document_id
      limit v_cap_precise
    ) d_sub on true
    where done.transaction_id is null
  ),

  s3_window_amount_exact as (
    select t.transaction_id, d_sub.document_id, 'window_amount_exact' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
        and (((not v_directional_matching) or t.tx_is_in) and t.credited is not null and t.credited > 0 and d.total = t.credited
          or ((not v_directional_matching) or t.tx_is_out) and t.debited is not null and t.debited > 0 and d.total = t.debited)
      order by d.document_date_unix, d.document_id
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s4_window_amount_tol as (
    select t.transaction_id, d_sub.document_id, 'window_amount_tol' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount union all select distinct transaction_id from s3_window_amount_exact) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
        and v_amount_tolerance_fraction > 0
        and (((not v_directional_matching) or t.tx_is_in) and t.credited is not null and t.credited > 0 and d.total between (t.credited / (1 + v_amount_tolerance_fraction)) and (t.credited / (1 - v_amount_tolerance_fraction))
          or ((not v_directional_matching) or t.tx_is_out) and t.debited is not null and t.debited > 0 and d.total between (t.debited / (1 + v_amount_tolerance_fraction)) and (t.debited / (1 - v_amount_tolerance_fraction)))
      order by d.document_date_unix, d.document_id
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s5_window_only as (
    select t.transaction_id, d_sub.document_id, 'window_only' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_customer_exact_amount union all select distinct transaction_id from s3_window_amount_exact union all select distinct transaction_id from s4_window_amount_tol) done using (transaction_id)
    join lateral (
      select d.document_id
      from invoice_scope d
      where d.company_id_text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and d.document_date_unix between (t.completed_date - v_date_window_seconds) and (t.completed_date + v_date_window_seconds)
      order by d.document_date_unix, d.document_id
      limit v_cap_window_only
    ) d_sub on true
    where done.transaction_id is null
  ),

  seeds as (
    select * from s1_invoice
    union all select * from s2_customer_exact_amount
    union all select * from s3_window_amount_exact
    union all select * from s4_window_amount_tol
    union all select * from s5_window_only
  ),

  seed_counts_total as (select seed, count(*) as ct from seeds group by seed),
  seed_counts_distinct as (select seed, count(distinct (transaction_id, document_id)) as ct from seeds group by seed),

  candidate_pairs as (select distinct transaction_id, document_id from seeds),

  distinct_pairs as (select distinct transaction_id, document_id from candidate_pairs),

  attributes as (
    select
      p.transaction_id,
      p.document_id,
      d.company_id_uuid as document_company_id,
      t.amount as transaction_amount,
      t.debited as transaction_debited,
      t.credited as transaction_credited,
      d.total as document_total,
      t.currency as transaction_currency,
      d.currency as document_currency,
      t.invoice_nr_norm as tx_invoice_norm,
      d.invoice_number_norm as doc_invoice_norm,
      d.invoice_number_raw as doc_invoice_raw,
      t.completed_date as transaction_date_unix,
      d.document_date_unix as document_date_unix,
      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_customer_display as transaction_customer_name,
      d.doc_customer_display as document_customer_name,
      t.tx_text_code,
      coalesce(nullif(d.doc_dedup_key,''), d.document_id::text) as doc_dedup_key,
      t.effective_amount as transaction_effective_amount,
      t.tx_is_in,
      t.tx_is_out
    from distinct_pairs p
    join transactions_scope t on t.transaction_id = p.transaction_id
    join invoice_scope    d on d.document_id    = p.document_id
    where t.company_id = d.company_id_text
  ),

  evaluated as (
    select
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,
      coalesce(((a.tx_invoice_norm is not null and a.doc_invoice_norm is not null and a.tx_invoice_norm = a.doc_invoice_norm) or (a.tx_text_code is not null and a.doc_invoice_norm is not null and position(a.doc_invoice_norm in a.tx_text_code) > 0)), false) as invoice_number_match,
      coalesce(a.transaction_currency  = a.document_currency, false) as currency_match,
      (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (a.tx_customer_ns = a.doc_customer_ns or position(a.doc_customer_ns in a.tx_customer_ns) > 0 or position(a.tx_customer_ns in a.doc_customer_ns) > 0)) as customer_match,
      coalesce(abs(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, false) as date_within_cap,
      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,
      (a.transaction_debited is not null and a.transaction_debited > 0 and a.document_total = a.transaction_debited) as amount_exact_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and a.document_total = a.transaction_credited) as amount_exact_from_credited,
      (a.transaction_debited is not null and a.transaction_debited > 0 and abs(a.document_total - a.transaction_debited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and abs(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_credited,
      a.tx_is_in,
      a.tx_is_out,
      (a.transaction_credited is not null and a.transaction_credited > 0) as credited_pos,
      (a.transaction_debited is not null and a.transaction_debited > 0) as debited_pos
    from attributes a
  ),

  with_amount_tolerance as (
    select
      e.*,
      case when v_directional_matching then (e.tx_is_out and e.amount_exact_from_debited) or (e.tx_is_in and e.amount_exact_from_credited) else (e.amount_exact_from_debited or e.amount_exact_from_credited) end as amount_exact_match,
      case when v_amount_tolerance_fraction > 0 then case when v_directional_matching then (e.tx_is_out and e.amount_tolerance_from_debited) or (e.tx_is_in and e.amount_tolerance_from_credited) else (e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) end else false end as amount_tolerance_match
    from evaluated e
  ),

  resolved_weights as (
    select
      coalesce((v_weights_json->>'amount_total')::numeric, 0) as amount_weight,
      coalesce((v_weights_json->>'invoice_number')::numeric, 0) as invoice_weight,
      coalesce((v_weights_json->>'date')::numeric, 0) as date_weight,
      coalesce((v_weights_json->>'currency')::numeric, 0) as currency_weight,
      coalesce((v_weights_json->>'customer_name')::numeric, 0) as customer_weight
  ),

  scored_components as (
    select
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,
      m.transaction_date_unix,
      m.document_date_unix,
      m.tx_is_in, m.tx_is_out,
      coalesce(case when m.amount_exact_match then w.amount_weight when m.amount_tolerance_match then w.amount_weight * exp(-0.5 * power((abs(m.transaction_effective_amount - m.document_total)) / nullif((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0), 2)) else 0 end, 0)::numeric as amount_score,
      (m.amount_exact_match or m.amount_tolerance_match) as amount_match,
      m.invoice_number_match as invoice_number_match,
      case when m.transaction_date_unix is not null and m.document_date_unix is not null then w.date_weight * exp(-0.5 * power(greatest(0, (abs(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days) / nullif(greatest(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k), 0), 2)) else 0 end::numeric as date_score,
      m.date_within_cap as date_match,
      m.currency_match as currency_match,
      m.customer_match as customer_match,
      w.amount_weight as amount_weight,
      w.invoice_weight as invoice_weight,
      w.date_weight as date_weight,
      w.currency_weight as currency_weight,
      w.customer_weight as customer_weight,
      (coalesce(m.invoice_number_match::int,0) * w.invoice_weight)::numeric as invoice_score,
      (coalesce(m.currency_match::int,0) * w.currency_weight)::numeric as currency_score,
      (coalesce(m.customer_match::int,0) * w.customer_weight)::numeric as customer_score,
      m.transaction_effective_amount,
      m.document_total
    from with_amount_tolerance m
    cross join resolved_weights w
  ),

  scored_with_confidence as (
    select
      s.*,
      (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight)::numeric as dynamic_total_weight,
      (s.amount_score + s.invoice_score + s.date_score + s.currency_score + s.customer_score)::numeric as total_numerator,
      (coalesce(s.amount_match::int,0) + coalesce(s.invoice_number_match::int,0) + coalesce(s.date_match::int,0) + coalesce(s.currency_match::int,0) + coalesce(s.customer_match::int,0)) as signals_matched_count,
      case when (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight) > 0 then round(((s.amount_score + s.invoice_score + s.date_score + s.currency_score + s.customer_score) / (s.amount_weight + s.invoice_weight + s.date_weight + s.currency_weight + s.customer_weight)) * 100.0, 4) else 0 end as confidence_percent
    from scored_components s
  ),

  low_signal as (select * from scored_with_confidence where signals_matched_count < 1),
  below_min_confidence as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent < v_config_min_confidence),
  base_candidates as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent >= v_config_min_confidence),

  ranked_per_transaction as (
    select b.*, row_number() over (partition by b.transaction_id order by b.confidence_percent desc, b.total_numerator desc, b.signals_matched_count desc, b.document_id) as rownum_per_transaction
    from base_candidates b
  ),
  not_top_for_transaction as (select * from ranked_per_transaction where rownum_per_transaction > 1),
  top_per_transaction as (select * from ranked_per_transaction where rownum_per_transaction = 1),

  ranked_per_document as (
    select t.*, row_number() over (partition by t.doc_dedup_key order by t.confidence_percent desc, t.total_numerator desc, t.signals_matched_count desc, abs(t.transaction_date_unix - t.document_date_unix) asc, t.document_id) as rownum_per_document
    from top_per_transaction t
  ),
  not_top_for_document as (select * from ranked_per_document where rownum_per_document > 1),
  top_per_document as (select * from ranked_per_document where rownum_per_document = 1),

  final_candidates as (
    select * from top_per_document
    order by confidence_percent desc, total_numerator desc, signals_matched_count desc, transaction_id, document_id
  ),

  allowed_for_insert as (
    select f.*
    from   final_candidates f
    where  not exists (select 1 from public.reconciliations r1 where r1.transaction_id = f.transaction_id)
       and not exists (select 1 from public.reconciliations r2 where r2.invoice_id    = f.document_id)
  ),

  inserted_reconciliations as (
    insert into public.reconciliations (
      transaction_id, invoice_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary, note
    )
    with ts_base as (
      select f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             coalesce(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint as base_unix
      from allowed_for_insert f
    ),
    enrich as (
      select
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match as invoice_match,
        d.amount_match, d.date_match, d.currency_match, d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        d.amount_score, d.amount_weight,
        d.date_score,   d.date_weight,
        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw,
        (a.tx_is_in or e.credited_pos) as tx_is_in_final,
        (a.tx_is_out or e.debited_pos) as tx_is_out_final
      from ts_base t
      join final_candidates d on d.transaction_id = t.transaction_id and d.document_id = t.document_id
      join evaluated        e on e.transaction_id = d.transaction_id and e.document_id = d.document_id
      join attributes       a on a.transaction_id = d.transaction_id and a.document_id = d.document_id
    ),
    notes as (
      select
        e.*,
        case when e.document_total is not null and e.document_total <> 0 then round(abs(e.transaction_effective_amount - e.document_total) / nullif(e.document_total,0) * 100.0, 2) else null end as amt_diff_pct,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) else null end as sec_diff,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) / 86400.0 else null end as day_gap,
        case when e.amount_weight > 0 then round((e.amount_score / e.amount_weight) * 100.0, 2) end as amount_ratio_pct,
        case when e.date_weight   > 0 then round((e.date_score   / e.date_weight)   * 100.0, 0)  end as date_ratio_pct
      from enrich e
    ),
    notes_text as (
      select
        n.*,
        case
          when n.date_weight > 0 and n.date_ratio_pct <= 99 then
            trim(both ' ' from concat_ws(' ',
              case when n.amount_weight > 0 and n.amount_score < n.amount_weight and n.amt_diff_pct is not null then concat('Amount scored ', coalesce(n.amount_ratio_pct::text,'0'), '% because payment vs invoice differed by ', n.amt_diff_pct::text, '%.') end,
              case when n.day_gap is not null then concat('Date scored ', coalesce(n.date_ratio_pct::text,'0'), '% because dates were ', round(n.day_gap::numeric, 2)::text, ' days apart.') end
            ))
          else null
        end as note_text
      from notes n
    )
    select
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      coalesce(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      extract(year from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      extract(quarter from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) at time zone 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          select json_agg(s) from unnest(array[
            case when e.invoice_match  then 'invoice'  end,
            case when e.amount_match   then 'amount'   end,
            case when e.date_match     then 'date'     end,
            case when e.currency_match then 'currency' end,
            case when e.customer_match then 'customer' end
          ]) s where s is not null
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  coalesce(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_is_in',     e.tx_is_in_final,
        'tx_is_out',    e.tx_is_out_final,
        'direction',    case when e.tx_is_in_final then 'incoming' when e.tx_is_out_final then 'outgoing' else 'unknown' end,
        'days_between_tx_and_doc',
          case when e.transaction_date_unix is not null and e.document_date_unix is not null then (abs(e.transaction_date_unix - e.document_date_unix) / 86400)::int else null end
      )::text,
      nullif(nt.note_text,'')
    from notes_text nt
    join enrich e using (transaction_id, document_id)
    returning reconciliation_id, transaction_id, invoice_id, confidence
  ),

  inserted_match_scores as (
    insert into public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score,  invoice_score,  order_score,  merchant_score,  amount_score,  currency_score,  customer_score
    )
    select
      r.reconciliation_id,
      d.document_company_id as company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      false as order_match,
      false as merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      0 as order_weight,
      0 as merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      0 as order_score,
      0 as merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    from inserted_reconciliations r
    join scored_with_confidence d
      on d.transaction_id = r.transaction_id
     and d.document_id    = r.invoice_id
    returning 1
  ),

  final_counts as (
    select
      (select count(*) from top_per_transaction) as top_per_transaction_count,
      (select count(*) from top_per_document) as top_per_document_count,
      (select count(*) from final_candidates) as passed_confidence_count,
      (select count(*) from inserted_reconciliations) as inserted_count,
      (select count(*) from base_candidates) as candidates_count,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_total) as seed_counts_total_json,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_distinct) as seed_counts_distinct_json,
      (select count(*) from transactions_scope) as total_tx_count,
      (select count(*) from invoice_scope) as total_doc_count
  )
  select
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  into
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  from final_counts;

  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', null,
    p_company_id, null, null, null,
    v_inserted_count, null, null, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered',  v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic',  v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  return jsonb_build_object(
    'msg', v_status_message,
    'time_ms', v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered',  v_total_docs,
    'top_per_tx', v_summary_top_per_tx,
    'top_per_doc', v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic',  v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

exception when others then
  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
  get stacked diagnostics
    v_err_state   = returned_sqlstate,
    v_err_msg     = message_text,
    v_err_detail  = pg_exception_detail,
    v_err_hint    = pg_exception_hint,
    v_err_context = pg_exception_context;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg,
    p_company_id, null, null, null,
    null, null, null, v_elapsed_ms,
    jsonb_build_object('sqlstate', v_err_state, 'detail', v_err_detail, 'hint', v_err_hint, 'context', v_err_context)
  );

  return jsonb_build_object(
    'msg','error','time_ms',v_elapsed_ms,
    'transactions_scanned',0,'documents_considered',0,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',v_err_msg,'sqlstate',v_err_state
  );
end;
$$;

alter function athena_reconcile_general_invoices_latest(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_invoices_latest(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_invoices_latest(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_invoices_latest(unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v1_9_o(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
declare
  v_status_message text := 'success';
  v_engine_version text := '1.9O';
  v_start_time timestamptz := clock_timestamp();
  v_elapsed_ms bigint := 0;

  v_config_amount_tolerance           numeric;
  v_config_date_window_days           numeric;
  v_config_min_confidence             numeric;
  v_config_allow_document_duplicates  boolean;
  v_config_log_activity               boolean;

  v_amount_tolerance_fraction numeric;
  v_date_window_seconds       bigint;
  v_date_full_weight_grace_days int := 30;

  v_edge_weight_small constant numeric := 0.10;
  v_gauss_k            constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small));

  v_cap_precise               int := 5;
  v_cap_broad                 int := 10;
  v_cap_window_only           int := 3;
  v_cap_iban_exact            int := 5;
  v_iban_exact_window_seconds bigint;
  v_max_transactions_to_scan  int := 20000;

  v_weights_json           jsonb := '{}'::jsonb;
  v_weights_total_possible numeric := 0;

  v_summary_top_per_tx   bigint := 0;
  v_summary_top_per_doc  bigint := 0;
  v_summary_passed_count bigint := 0;
  v_inserted_count       bigint := 0;
  v_candidates_count     bigint := 0;

  v_total_tx   bigint := 0;
  v_total_docs bigint := 0;

  v_seed_counts_total    jsonb := '{}'::jsonb;
  v_seed_counts_distinct jsonb := '{}'::jsonb;

  v_log_run_id        uuid := gen_random_uuid();
  v_log_run_unix_time bigint := extract(epoch from now())::bigint;

  v_directional_matching boolean := true;

  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;
begin
  select amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  into   v_config_amount_tolerance, v_config_date_window_days, v_config_min_confidence,
         v_config_allow_document_duplicates, v_config_log_activity
  from   public.reconciliations_config
  where  reconciliation_config_id = p_reconciliation_config_id;

  if not found then
    v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
    return jsonb_build_object('msg','reconciliations_config not found','time_ms',v_elapsed_ms,
                              'transactions_scanned',0,'documents_considered',0,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  end if;

  v_amount_tolerance_fraction := case
    when v_config_amount_tolerance >= 1 then v_config_amount_tolerance / 100.0
    else v_config_amount_tolerance
  end;

  v_date_window_seconds := ceil(coalesce(v_config_date_window_days, 1) * 86400)::bigint;
  v_iban_exact_window_seconds := least(v_date_window_seconds, 14 * 86400)::bigint;

  if v_config_min_confidence is null then v_config_min_confidence := 0; end if;
  if v_config_min_confidence <= 1 then v_config_min_confidence := v_config_min_confidence * 100.0; end if;

  with candidate_weights as (
    select lower(trim(w.weight_key)) as k_norm,
           (w.weight_value)::numeric as v_norm,
           w.company_id
    from public.reconciliations_weight_registry w
    where w.active
      and w.merchant_id is null
      and (
           (p_company_id is null and w.company_id is null)
        or (p_company_id is not null and (w.company_id = p_company_id or w.company_id is null))
      )
      and lower(trim(w.weight_key)) in (
        'amount_total','invoice_number','order_number','date','currency','merchant_id','customer_name'
      )
  ),
  picked as (
    select distinct on (k_norm) k_norm, v_norm
    from candidate_weights
    order by k_norm, (company_id is null)
  )
  select coalesce(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) into v_weights_json from picked;

  select coalesce((v_weights_json->>'amount_total')::numeric,0)
       + coalesce((v_weights_json->>'invoice_number')::numeric,0)
       + coalesce((v_weights_json->>'order_number')::numeric,0)
       + coalesce((v_weights_json->>'date')::numeric,0)
       + coalesce((v_weights_json->>'currency')::numeric,0)
       + coalesce((v_weights_json->>'merchant_id')::numeric,0)
       + coalesce((v_weights_json->>'customer_name')::numeric,0)
  into v_weights_total_possible;

  if v_weights_total_possible = 0 then
    v_weights_json := jsonb_build_object(
      'amount_total',1,'invoice_number',1,'order_number',1,'date',1,'currency',1,'merchant_id',1,'customer_name',1
    );
    v_weights_total_possible := 7;
  end if;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_start', 'function', 'ok', null,
    p_company_id, null, null, null,
    null, null, null, null,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', v_config_log_activity,
        'allow_doc_dups', v_config_allow_document_duplicates,
        'amount_tol_pct', v_amount_tolerance_fraction,
        'min_confidence', v_config_min_confidence,
        'date_window_days', v_config_date_window_days,
        'max_tx_seed', v_max_transactions_to_scan,
        'cap_precise', v_cap_precise,
        'cap_broad',   v_cap_broad,
        'cap_window_only', v_cap_window_only,
        'cap_iban_exact', v_cap_iban_exact,
        'iban_exact_window_days', round(v_iban_exact_window_seconds/86400.0,2),
        'directional_matching', v_directional_matching,
        'gauss_edge_weight', v_edge_weight_small
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  with
  document_scope as materialized (
    select
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      nullif(d.invoice_number, '') as invoice_number_raw,
      nullif(d.order_id, '')       as order_id_raw,
      nullif(public.norm_code(d.invoice_number), '') as invoice_number_norm,
      nullif(public.norm_code(d.order_id), '')       as order_id_norm,
      d.document_date_unix::bigint as document_date_unix,
      d.merchant_id,
      d.customer_name,
      nullif(public.norm_core_ns(coalesce(d.merchant_name, d.merchant_info::jsonb->>'name')), '') as doc_merchant_ns,
      nullif(public.norm_text_ns(
        concat_ws(' ',
          d.customer_name,
          d.customer_info::jsonb->>'name',
          d.customer_info::jsonb->>'contact_person',
          d.customer_info::jsonb->>'company_name',
          d.customer_info::jsonb->>'email'
        )
      ), '') as doc_customer_ns,
      nullif(public.norm_iban(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '')      as doc_iban_ns,
      nullif(public.norm_iban_core(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '') as doc_iban_core_ns,
      coalesce(
        d.customer_name,
        d.customer_info::jsonb->>'name',
        d.customer_info::jsonb->>'contact_person',
        d.customer_info::jsonb->>'company_name',
        d.customer_info::jsonb->>'email'
      ) as doc_customer_display,
      coalesce(nullif(d.dedup_serialized,''), d.document_id::text) as doc_dedup_key
    from public.documents_v2 d
    where (p_company_id is null or d.company_id = p_company_id)
      and d.reconciled is not true
  ),

  transactions_scope as materialized (
    select
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.invoice_nr,''),
            (select m[1]
             from regexp_matches(
               upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
               '(INV(?:OICE)?[ -_/]?\d{1,20}|[A-Z]{1,6}[-/]\d{2,20}|[0-9]{4}[-/][0-9]{2,20})',
               'g'
             ) as m
             limit 1)
          )
        ),
      '') as invoice_nr_norm,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.order_id,''),
            (select m[1]
             from regexp_matches(
               upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
               '(ORD(?:ER)?[ -_/]?\d{2,20}|PO[ -_/]?\d{2,20}|ORDER[ -_/]?\d{2,20})',
               'g'
             ) as m
             limit 1)
          )
        ),
      '') as order_id_norm,
      t.completed_date::bigint as completed_date,
      t.merchant_id,
      t.name_consumer,
      (nullif(t.credited,0) is not null or nullif(t.money_in,0)  is not null)  as tx_is_in,
      (nullif(t.debited,0)  is not null or nullif(t.money_out,0) is not null) as tx_is_out,
      nullif(public.norm_core_ns(
        coalesce(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
      ), '') as tx_merchant_ns,
      nullif(public.norm_text_ns(
        coalesce(
          t.name_consumer,
          t.account_holder_name,
          t.account_holder_name_counterparty
        )
      ), '') as tx_customer_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '')      as tx_iban_beneficiary_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '')             as tx_iban_counterparty_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '') as tx_iban_beneficiary_core_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '')        as tx_iban_counterparty_core_ns,
      coalesce(
        t.name_consumer,
        t.account_holder_name,
        t.account_holder_name_counterparty
      ) as tx_customer_display,
      case
        when t.debited  is not null and t.debited  > 0 then t.debited
        when t.credited is not null and t.credited > 0 then t.credited
        when t.amount   is not null                  then abs(t.amount)
        else null
      end as effective_amount
    from public.transactions t
    where (p_company_id is null or t.company_id = p_company_id::text)
      and t.reconciled is not true
      and (
           t.invoice_nr is not null
        or t.order_id   is not null
        or (t.currency is not null and t.merchant_id   is not null)
        or (t.currency is not null and (t.name_consumer is not null or t.account_holder_name is not null or t.account_holder_name_counterparty is not null))
        or (t.currency is not null and (t.amount is not null or t.debited is not null or t.credited is not null))
      )
    order by t.completed_date desc, t.transaction_id
    limit v_max_transactions_to_scan
  ),

  s1_invoice as (
    select t.transaction_id, d.document_id, 'invoice_number' as seed
    from transactions_scope t
    join document_scope d
      on d.company_id::text = t.company_id
     and d.invoice_number_norm is not null
     and t.invoice_nr_norm     is not null
     and d.invoice_number_norm = t.invoice_nr_norm
     and d.currency = t.currency
  ),

  s2_order as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'order_number' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and d.order_id_norm is not null
       and t.order_id_norm is not null
       and d.order_id_norm = t.order_id_norm
       and d.currency = t.currency
    ) s
    left join (select distinct transaction_id from s1_invoice) u using (transaction_id)
    where u.transaction_id is null
  ),

  s3_iban_exact_amount_out as (
    select t.transaction_id, d_sub.document_id, 'iban_exact_amount_out' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and t.debited is not null and t.debited > 0
        and d.total = t.debited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_sub on true
    where done.transaction_id is null
  ),
  s3_iban_exact_amount_in as (
    select t.transaction_id, d_sub.document_id, 'iban_exact_amount_in' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and t.credited is not null and t.credited > 0
        and d.total = t.credited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_sub on true
    where done.transaction_id is null
  ),
  s3_iban_exact_amount as (
    select * from s3_iban_exact_amount_out
    union all
    select * from s3_iban_exact_amount_in
  ),

  s4_merchant_id_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_id_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.merchant_id = t.merchant_id
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s5_merchant_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
       and (
         d.doc_merchant_ns = t.tx_merchant_ns
         or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
         or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
       )
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s6_customer_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'customer_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_in)
       and d.currency = t.currency
       and d.doc_customer_ns is not null and t.tx_customer_ns is not null
       and (
         d.doc_customer_ns = t.tx_customer_ns
         or position(d.doc_customer_ns in t.tx_customer_ns) > 0
         or position(t.tx_customer_ns in d.doc_customer_ns) > 0
       )
       and t.credited is not null and t.credited > 0
       and d.total = t.credited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s7_iban_plus_signal as (
    select t.transaction_id, d_sub.document_id, 'iban_plus_signal' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and (
             ( ((not v_directional_matching) or t.tx_is_out) and
               ( (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
                 or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns) ) )
          or ( ((not v_directional_matching) or t.tx_is_in ) and
               ( (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
                 or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns) ) )
        )
        and (
             (t.invoice_nr_norm is not null and d.invoice_number_norm is not null)
          or (t.order_id_norm   is not null and d.order_id_norm   is not null)
          or (t.tx_merchant_ns  is not null and d.doc_merchant_ns is not null)
          or (t.tx_customer_ns  is not null and d.doc_customer_ns is not null)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_precise
    ) d_sub on true
    where done.transaction_id is null
  ),

  s8_merchant_id_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_id_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.merchant_id = t.merchant_id
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction))
                        and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s9_merchant_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
        and (
          d.doc_merchant_ns = t.tx_merchant_ns
          or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
          or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
        )
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction))
                        and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s10_customer_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'customer_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and d.doc_customer_ns is not null and t.tx_customer_ns is not null
        and (
          d.doc_customer_ns = t.tx_customer_ns
          or position(d.doc_customer_ns in t.tx_customer_ns) > 0
          or position(t.tx_customer_ns in d.doc_customer_ns) > 0
        )
        and v_amount_tolerance_fraction > 0
        and t.credited is not null and t.credited > 0
        and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                        and (t.credited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.credited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s11_iban_tol as (
    select t.transaction_id, d_sub.document_id, 'iban_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and
            ( (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns) )
            and t.debited is not null and t.debited > 0
            and d.total between (t.debited  / (1 + v_amount_tolerance_fraction))
                            and (t.debited  / (1 - v_amount_tolerance_fraction)) )
          or
          ( ((not v_directional_matching) or t.tx_is_in) and
            ( (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns) )
            and t.credited is not null and t.credited > 0
            and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                            and (t.credited / (1 - v_amount_tolerance_fraction)) )
        )
      order by
        coalesce(abs(d.total - t.debited), abs(d.total - t.credited)),
        abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s12_window_amount_exact as (
    select t.transaction_id, d_sub.document_id, 'window_amount_exact' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and t.debited  is not null and t.debited  > 0 and d.total = t.debited )
          or
          ( ((not v_directional_matching) or t.tx_is_in ) and t.credited is not null and t.credited > 0 and d.total = t.credited)
        )
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s13_window_amount_tol as (
    select t.transaction_id, d_sub.document_id, 'window_amount_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and t.debited  is not null and t.debited  > 0
            and d.total between (t.debited  / (1 + v_amount_tolerance_fraction))
                            and (t.debited  / (1 - v_amount_tolerance_fraction)) )
          or
          ( ((not v_directional_matching) or t.tx_is_in ) and t.credited is not null and t.credited > 0
            and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                            and (t.credited / (1 - v_amount_tolerance_fraction)) )
        )
      order by
        coalesce(abs(d.total - t.debited), abs(d.total - t.credited)),
        abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s14_window_only as (
    select t.transaction_id, d_sub.document_id, 'window_only' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
      union all select distinct transaction_id from s13_window_amount_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_window_only
    ) d_sub on true
    where done.transaction_id is null
  ),

  seeds as (
    select * from s1_invoice
    union all select * from s2_order
    union all select * from s3_iban_exact_amount
    union all select * from s4_merchant_id_exact_amount
    union all select * from s5_merchant_ns_exact_amount
    union all select * from s6_customer_ns_exact_amount
    union all select * from s7_iban_plus_signal
    union all select * from s8_merchant_id_tol
    union all select * from s9_merchant_ns_tol
    union all select * from s10_customer_ns_tol
    union all select * from s11_iban_tol
    union all select * from s12_window_amount_exact
    union all select * from s13_window_amount_tol
    union all select * from s14_window_only
  ),

  seed_counts_total as (
    select seed, count(*) as ct from seeds group by seed
  ),
  seed_counts_distinct as (
    select seed, count(distinct (transaction_id, document_id)) as ct from seeds group by seed
  ),

  candidate_pairs as (
    select distinct transaction_id, document_id from seeds
  ),

  distinct_pairs as (
    select distinct transaction_id, document_id
    from candidate_pairs
  ),

  attributes as (
    select
      p.transaction_id,
      p.document_id,
      t.company_id::uuid as transaction_company_id,
      d.company_id       as document_company_id,
      t.amount as transaction_amount,
      t.debited as transaction_debited,
      t.credited as transaction_credited,
      d.total  as document_total,
      t.currency as transaction_currency,
      d.currency as document_currency,
      t.invoice_nr_norm as tx_invoice_norm,
      d.invoice_number_norm as doc_invoice_norm,
      t.order_id_norm  as tx_order_norm,
      d.order_id_norm  as doc_order_norm,
      d.invoice_number_raw as doc_invoice_raw,
      d.order_id_raw       as doc_order_raw,
      t.completed_date as transaction_date_unix,
      d.document_date_unix as document_date_unix,
      t.merchant_id as transaction_merchant_id,
      d.merchant_id as document_merchant_id,
      t.tx_customer_display as transaction_customer_name,
      d.doc_customer_display as document_customer_name,
      t.tx_is_in, t.tx_is_out,
      t.tx_merchant_ns, d.doc_merchant_ns,
      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_iban_beneficiary_ns,
      t.tx_iban_counterparty_ns,
      d.doc_iban_ns,
      t.tx_iban_beneficiary_core_ns,
      t.tx_iban_counterparty_core_ns,
      d.doc_iban_core_ns,
      coalesce(nullif(d.doc_dedup_key,''), d.document_id::text) as doc_dedup_key,
      t.effective_amount as transaction_effective_amount
    from distinct_pairs p
    join transactions_scope t on t.transaction_id = p.transaction_id
    join document_scope    d on d.document_id    = p.document_id
    where d.company_id::text = t.company_id
  ),

  evaluated as (
    select
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,
      coalesce((a.tx_invoice_norm is not null and a.doc_invoice_norm is not null
                and a.tx_invoice_norm = a.doc_invoice_norm), false) as invoice_number_match,
      coalesce((a.tx_order_norm   is not null and a.doc_order_norm   is not null
                and a.tx_order_norm = a.doc_order_norm), false)     as order_number_match,
      coalesce(a.transaction_currency  = a.document_currency, false) as currency_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_out and (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null
                  and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or (
                   (a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns)
                or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            ))
          else
            (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null
                  and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or (
                   (a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns)
                or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            )
        end
      ) as merchant_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_in and (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (
                    a.tx_customer_ns = a.doc_customer_ns
                 or position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or (
                   (a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            ))
          else
            (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (
                    a.tx_customer_ns = a.doc_customer_ns
                 or position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or (
                   (a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            )
        end
      ) as customer_match,
      coalesce(abs(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, false) as date_within_cap,
      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,
      (a.transaction_debited  is not null and a.transaction_debited  > 0 and a.document_total = a.transaction_debited)  as amount_exact_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and a.document_total = a.transaction_credited) as amount_exact_from_credited,
      (a.transaction_debited  is not null and a.transaction_debited  > 0 and abs(a.document_total - a.transaction_debited)  <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and abs(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_credited,
      a.tx_is_in, a.tx_is_out,
      a.tx_iban_beneficiary_ns,
      a.tx_iban_counterparty_ns,
      a.doc_iban_ns,
      a.tx_iban_beneficiary_core_ns,
      a.tx_iban_counterparty_core_ns,
      a.doc_iban_core_ns,
      a.tx_merchant_ns,
      a.doc_merchant_ns,
      a.tx_customer_ns,
      a.doc_customer_ns,
      a.transaction_merchant_id,
      a.document_merchant_id
    from attributes a
  ),

  with_amount_tolerance as (
    select
      e.*,
      case
        when e.tx_is_out and e.amount_exact_from_debited then true
        when e.tx_is_in  and e.amount_exact_from_credited then true
        when not v_directional_matching and (e.amount_exact_from_debited or e.amount_exact_from_credited) then true
        else false
      end as amount_exact_match,
      case
        when v_amount_tolerance_fraction > 0 then
          case
            when e.tx_is_out and e.amount_tolerance_from_debited then true
            when e.tx_is_in  and e.amount_tolerance_from_credited then true
            when not v_directional_matching and (e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) then true
            else false
          end
        else false
      end as amount_tolerance_match,
      (e.amount_exact_from_debited or e.amount_exact_from_credited or e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) as amount_direction_ok
    from evaluated e
  ),

  resolved_weights as (
    select
      coalesce((v_weights_json->>'amount_total')::numeric, 0)   as amount_weight,
      coalesce((v_weights_json->>'invoice_number')::numeric, 0) as invoice_weight,
      coalesce((v_weights_json->>'order_number')::numeric, 0)   as order_weight,
      coalesce((v_weights_json->>'date')::numeric, 0)           as date_weight,
      coalesce((v_weights_json->>'currency')::numeric, 0)       as currency_weight,
      coalesce((v_weights_json->>'merchant_id')::numeric, 0)    as merchant_weight,
      coalesce((v_weights_json->>'customer_name')::numeric, 0)  as customer_weight
  ),

  scored_components as (
    select
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,
      m.transaction_date_unix,
      m.document_date_unix,
      m.amount_direction_ok,
      m.tx_is_in, m.tx_is_out,
      m.tx_iban_beneficiary_ns,
      m.tx_iban_counterparty_ns,
      m.doc_iban_ns,
      m.tx_iban_beneficiary_core_ns,
      m.tx_iban_counterparty_core_ns,
      m.doc_iban_core_ns,
      m.tx_merchant_ns,
      m.doc_merchant_ns,
      m.tx_customer_ns,
      m.doc_customer_ns,
      m.transaction_merchant_id,
      m.document_merchant_id,
      coalesce(
        case
          when m.amount_exact_match then w.amount_weight
          when m.amount_tolerance_match then
            w.amount_weight * exp(
              -0.5 * power(
                (abs(m.transaction_effective_amount - m.document_total))
                / nullif((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0)
              , 2)
            )
          else 0
        end, 0
      )::numeric as amount_score,
      (m.amount_exact_match or m.amount_tolerance_match) as amount_match,
      m.invoice_number_match  as invoice_number_match,
      m.order_number_match    as order_number_match,
      case
        when m.transaction_date_unix is not null and m.document_date_unix is not null then
          w.date_weight * exp(
            -0.5 * power(
              greatest(0,
                (abs(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days
              ) / nullif( greatest(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k ), 0)
            , 2)
          )
        else 0
      end::numeric as date_score,
      m.date_within_cap as date_match,
      m.currency_match  as currency_match,
      m.merchant_match  as merchant_match,
      m.customer_match  as customer_match,
      w.amount_weight   as amount_weight,
      w.invoice_weight  as invoice_weight,
      w.order_weight    as order_weight,
      w.date_weight     as date_weight,
      w.currency_weight as currency_weight,
      w.merchant_weight as merchant_weight,
      w.customer_weight as customer_weight,
      (coalesce(m.invoice_number_match::int,0)  * w.invoice_weight)::numeric  as invoice_score,
      (coalesce(m.order_number_match::int,0)    * w.order_weight)::numeric    as order_score,
      (coalesce(m.currency_match::int,0)        * w.currency_weight)::numeric as currency_score,
      (coalesce(m.merchant_match::int,0)        * w.merchant_weight)::numeric as merchant_score,
      (coalesce(m.customer_match::int,0)        * w.customer_weight)::numeric as customer_score
    from with_amount_tolerance m
    cross join resolved_weights w
  ),

  scored_with_applicability as (
    select
      s.*,
      (
        case
          when v_directional_matching then
            ( s.tx_is_out and (
                 (s.doc_merchant_ns is not null and s.tx_merchant_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              or (s.document_merchant_id is not null and s.transaction_merchant_id is not null)
            ))
          else
            (
                 (s.doc_merchant_ns is not null and s.tx_merchant_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              or (s.document_merchant_id is not null and s.transaction_merchant_id is not null)
            )
        end
      ) as merchant_applicable,
      (
        case
          when v_directional_matching then
            ( s.tx_is_in and (
                 (s.doc_customer_ns is not null and s.tx_customer_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            ))
          else
            (
                 (s.doc_customer_ns is not null and s.tx_customer_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            )
        end
      ) as customer_applicable
    from scored_components s
  ),

  scored_with_confidence as (
    select
      s.*,
      (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      )::numeric as dynamic_total_weight,
      (
        s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
        + (case when s.merchant_applicable then s.merchant_score else 0 end)
        + (case when s.customer_applicable then s.customer_score else 0 end)
      )::numeric as total_numerator,
      (coalesce(s.amount_match::int,0)
     + coalesce(s.invoice_number_match::int,0)
     + coalesce(s.order_number_match::int,0)
     + coalesce(s.date_match::int,0)
     + coalesce(s.currency_match::int,0)
     + coalesce(s.merchant_match::int,0)
     + coalesce(s.customer_match::int,0)) as signals_matched_count,
      case when (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      ) > 0 then
        round(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
              + (case when s.merchant_applicable then s.merchant_score else 0 end)
              + (case when s.customer_applicable then s.customer_score else 0 end))
            / (s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
              + (case when s.merchant_applicable then s.merchant_weight else 0 end)
              + (case when s.customer_applicable then s.customer_weight else 0 end))) * 100.0, 4)
      else 0 end as confidence_percent
    from scored_with_applicability s
  ),

  low_signal as (select * from scored_with_confidence where signals_matched_count < 1),
  below_min_confidence as (
    select * from scored_with_confidence
    where signals_matched_count >= 1 and confidence_percent < v_config_min_confidence
  ),
  base_candidates as (
    select * from scored_with_confidence
    where signals_matched_count >= 1 and confidence_percent >= v_config_min_confidence
  ),

  ranked_per_transaction as (
    select
      b.*,
      row_number() over (
        partition by b.transaction_id
        order by b.confidence_percent desc, b.total_numerator desc, b.signals_matched_count desc, b.document_id
      ) as rownum_per_transaction
    from base_candidates b
  ),
  not_top_for_transaction as (select * from ranked_per_transaction where rownum_per_transaction > 1),
  top_per_transaction as (select * from ranked_per_transaction where rownum_per_transaction = 1),

  ranked_per_document as (
    select
      t.*,
      row_number() over (
        partition by t.doc_dedup_key
        order by
          t.confidence_percent desc,
          t.total_numerator desc,
          t.signals_matched_count desc,
          abs(t.transaction_date_unix - t.document_date_unix) asc,
          t.document_id
      ) as rownum_per_document
    from top_per_transaction t
  ),
  not_top_for_document as (select * from ranked_per_document where rownum_per_document > 1),
  top_per_document as (select * from ranked_per_document where rownum_per_document = 1),

  final_candidates as (
    select *
    from top_per_document
    order by confidence_percent desc, total_numerator desc, signals_matched_count desc, transaction_id, document_id
  ),

  allowed_for_insert as (
    select f.*
    from   final_candidates f
    where  not exists (select 1 from public.reconciliations r1 where r1.transaction_id = f.transaction_id)
       and not exists (select 1 from public.reconciliations r2 where r2.document_id    = f.document_id)
  ),

  inserted_reconciliations as (
    insert into public.reconciliations (
      transaction_id, document_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary, note
    )
    with ts_base as (
      select f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             coalesce(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint as base_unix
      from allowed_for_insert f
    ),
    enrich as (
      select
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match as invoice_match,
        d.order_number_match   as order_match,
        d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        d.amount_score, d.amount_weight,
        d.date_score,   d.date_weight,
        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm, a.tx_order_norm, a.doc_order_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw, a.doc_order_raw,
        a.tx_iban_beneficiary_ns, a.tx_iban_counterparty_ns, a.doc_iban_ns,
        a.tx_iban_beneficiary_core_ns, a.tx_iban_counterparty_core_ns, a.doc_iban_core_ns,
        a.tx_is_in, a.tx_is_out
      from ts_base t
      join final_candidates d on d.transaction_id = t.transaction_id and d.document_id = t.document_id
      join attributes a       on a.transaction_id = d.transaction_id and a.document_id = d.document_id
    ),
    notes as (
      select
        e.*,
        case
          when e.document_total is not null and e.document_total <> 0
          then round(abs(e.transaction_effective_amount - e.document_total) / nullif(e.document_total,0) * 100.0, 2)
          else null
        end as amt_diff_pct,
        case
          when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix)
          else null
        end as sec_diff,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 86400.0
          else null end as day_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 3600.0
          else null end as hour_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 60.0
          else null end as minute_gap,
        case when e.amount_weight > 0 then round((e.amount_score / e.amount_weight) * 100.0, 2) end as amount_ratio_pct,
        case when e.date_weight   > 0 then round((e.date_score   / e.date_weight)   * 100.0, 0)  end as date_ratio_pct
      from enrich e
    ),
    notes_text as (
      select
        n.*,
        case
          when n.date_weight > 0 and n.date_ratio_pct <= 99 then
            trim(both ' ' from concat_ws(' ',
              case
                when n.amount_weight > 0 and n.amount_score < n.amount_weight and n.amt_diff_pct is not null
                then concat('Amount scored ', coalesce(n.amount_ratio_pct::text,'0'),
                            '% because payment vs document differed by ', n.amt_diff_pct::text, '%.')
              end,
              case
                when n.day_gap is not null
                then concat('Date scored ', coalesce(n.date_ratio_pct::text,'0'),
                            '% because dates were ', round(n.day_gap::numeric, 2)::text, ' days apart.')
              end
            ))
          else null
        end as note_text
      from notes n
    )
    select
      e.transaction_id, e.document_id, e.document_company_id,
      coalesce(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      extract(year from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      extract(quarter from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) at time zone 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          select json_agg(s) from unnest(array[
            case when e.invoice_match  then 'invoice'  end,
            case when e.amount_match   then 'amount'   end,
            case when e.date_match     then 'date'     end,
            case when e.currency_match then 'currency' end,
            case when e.merchant_match then 'merchant' end,
            case when e.customer_match then 'customer' end,
            case when e.order_match    then 'order'    end
          ]) s where s is not null
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  coalesce(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_order',     e.tx_order_norm,
        'doc_order',    coalesce(e.doc_order_raw, e.doc_order_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_iban_beneficiary',        e.tx_iban_beneficiary_ns,
        'tx_iban_counterparty',       e.tx_iban_counterparty_ns,
        'doc_iban',                   e.doc_iban_ns,
        'tx_iban_beneficiary_core',   e.tx_iban_beneficiary_core_ns,
        'tx_iban_counterparty_core',  e.tx_iban_counterparty_core_ns,
        'doc_iban_core',              e.doc_iban_core_ns,
        'tx_is_in',    e.tx_is_in,
        'tx_is_out',   e.tx_is_out,
        'direction',   case when e.tx_is_in then 'incoming' when e.tx_is_out then 'outgoing' else 'unknown' end,
        'days_between_tx_and_doc',
          case
            when e.transaction_date_unix is not null and e.document_date_unix is not null
              then (abs(e.transaction_date_unix - e.document_date_unix) / 86400)::int
            else null
          end
      )::text,
      nullif(nt.note_text,'')
    from notes_text nt
    join enrich e using (transaction_id, document_id)
    returning reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores as (
    insert into public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score,  invoice_score,  order_score,  merchant_score,  amount_score,  currency_score,  customer_score
    )
    select
      r.reconciliation_id,
      d.document_company_id as company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      d.order_number_match,
      d.merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      d.order_weight,
      d.merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      d.order_score,
      d.merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    from inserted_reconciliations r
    join scored_with_confidence d
      on d.transaction_id = r.transaction_id
     and d.document_id    = r.document_id
    returning 1
  ),

  final_counts as (
    select
      (select count(*) from top_per_transaction)      as top_per_transaction_count,
      (select count(*) from top_per_document)         as top_per_document_count,
      (select count(*) from final_candidates)         as passed_confidence_count,
      (select count(*) from inserted_reconciliations) as inserted_count,
      (select count(*) from base_candidates)          as candidates_count,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_total)    as seed_counts_total_json,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_distinct) as seed_counts_distinct_json,
      (select count(*) from transactions_scope)       as total_tx_count,
      (select count(*) from document_scope)           as total_doc_count
  )
  select
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  into
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  from final_counts;

  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', null,
    p_company_id, null, null, null,
    v_inserted_count, null, null, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered',  v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic',  v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  return jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered',  v_total_docs,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic',  v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

exception when others then
  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
  get stacked diagnostics
    v_err_state   = returned_sqlstate,
    v_err_msg     = message_text,
    v_err_detail  = pg_exception_detail,
    v_err_hint    = pg_exception_hint,
    v_err_context = pg_exception_context;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg,
    p_company_id, null, null, null,
    null, null, null, v_elapsed_ms,
    jsonb_build_object(
      'sqlstate', v_err_state,
      'detail',   v_err_detail,
      'hint',     v_err_hint,
      'context',  v_err_context
    )
  );

  return jsonb_build_object(
    'msg','error','time_ms',v_elapsed_ms,
    'transactions_scanned',0,'documents_considered',0,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',v_err_msg,'sqlstate',v_err_state
  );
end;
$$;

alter function athena_reconcile_general_v1_9_o(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v1_9_o(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v1_9_o(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v1_9_o(unknown, unknown) to service_role;

create or replace function athena_reconcile_general_latest(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
declare
  v_status_message text := 'success';
  v_engine_version text := 'v1_9_P';
  v_start_time timestamptz := clock_timestamp();
  v_elapsed_ms bigint := 0;

  v_config_amount_tolerance numeric;
  v_config_date_window_days numeric;
  v_config_min_confidence numeric;
  v_config_allow_document_duplicates boolean;
  v_config_log_activity boolean;

  v_amount_tolerance_fraction numeric;
  v_date_window_seconds bigint;
  v_date_full_weight_grace_days int := 30;

  v_edge_weight_small constant numeric := 0.10;
  v_gauss_k constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small));

  v_cap_precise int := 5;
  v_cap_broad int := 10;
  v_cap_window_only int := 3;
  v_cap_iban_exact int := 5;
  v_iban_exact_window_seconds bigint;
  v_max_transactions_to_scan int := 20000;

  v_weights_json jsonb := '{}'::jsonb;
  v_weights_total_possible numeric := 0;

  v_summary_top_per_tx bigint := 0;
  v_summary_top_per_doc bigint := 0;
  v_summary_passed_count bigint := 0;
  v_inserted_count bigint := 0;
  v_candidates_count bigint := 0;

  v_total_tx bigint := 0;
  v_total_docs bigint := 0;

  v_seed_counts_total jsonb := '{}'::jsonb;
  v_seed_counts_distinct jsonb := '{}'::jsonb;

  v_log_run_id uuid := gen_random_uuid();
  v_log_run_unix_time bigint := extract(epoch from now())::bigint;

  v_directional_matching boolean := true;

  v_err_state text;
  v_err_msg text;
  v_err_detail text;
  v_err_hint text;
  v_err_context text;
begin
  select amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  into v_config_amount_tolerance, v_config_date_window_days, v_config_min_confidence, v_config_allow_document_duplicates, v_config_log_activity
  from public.reconciliations_config
  where reconciliation_config_id = p_reconciliation_config_id;

  if not found then
    v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
    return jsonb_build_object('msg','reconciliations_config not found','time_ms',v_elapsed_ms,'transactions_scanned',0,'documents_considered',0,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  end if;

  v_amount_tolerance_fraction := case when v_config_amount_tolerance >= 1 then v_config_amount_tolerance / 100.0 else v_config_amount_tolerance end;
  v_date_window_seconds := ceil(coalesce(v_config_date_window_days, 1) * 86400)::bigint;
  v_iban_exact_window_seconds := least(v_date_window_seconds, 14 * 86400)::bigint;

  if v_config_min_confidence is null then v_config_min_confidence := 0; end if;
  if v_config_min_confidence <= 1 then v_config_min_confidence := v_config_min_confidence * 100.0; end if;

  with candidate_weights as (
    select lower(trim(w.weight_key)) as k_norm, (w.weight_value)::numeric as v_norm, w.company_id
    from public.reconciliations_weight_registry w
    where w.active
      and w.merchant_id is null
      and ((p_company_id is null and w.company_id is null) or (p_company_id is not null and (w.company_id = p_company_id or w.company_id is null)))
      and lower(trim(w.weight_key)) in ('amount_total','invoice_number','order_number','date','currency','merchant_id','customer_name')
  ),
  picked as (
    select distinct on (k_norm) k_norm, v_norm
    from candidate_weights
    order by k_norm, (company_id is null)
  )
  select coalesce(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) into v_weights_json from picked;

  select coalesce((v_weights_json->>'amount_total')::numeric,0)
       + coalesce((v_weights_json->>'invoice_number')::numeric,0)
       + coalesce((v_weights_json->>'order_number')::numeric,0)
       + coalesce((v_weights_json->>'date')::numeric,0)
       + coalesce((v_weights_json->>'currency')::numeric,0)
       + coalesce((v_weights_json->>'merchant_id')::numeric,0)
       + coalesce((v_weights_json->>'customer_name')::numeric,0)
  into v_weights_total_possible;

  if v_weights_total_possible = 0 then
    v_weights_json := jsonb_build_object('amount_total',1,'invoice_number',1,'order_number',1,'date',1,'currency',1,'merchant_id',1,'customer_name',1);
    v_weights_total_possible := 7;
  end if;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message, company_id, transaction_id, document_id, reconciliation_id, rows_affected, confidence, step_ms, processing_ms, context
  ) values (
    v_log_run_id, v_engine_version, 'run_start', 'function', 'ok', null, p_company_id, null, null, null, null, null, null, null,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', v_config_log_activity,
        'allow_doc_dups', v_config_allow_document_duplicates,
        'amount_tol_pct', v_amount_tolerance_fraction,
        'min_confidence', v_config_min_confidence,
        'date_window_days', v_config_date_window_days,
        'max_tx_seed', v_max_transactions_to_scan,
        'cap_precise', v_cap_precise,
        'cap_broad', v_cap_broad,
        'cap_window_only', v_cap_window_only,
        'cap_iban_exact', v_cap_iban_exact,
        'iban_exact_window_days', round(v_iban_exact_window_seconds/86400.0,2),
        'directional_matching', v_directional_matching,
        'gauss_edge_weight', v_edge_weight_small
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  with
  document_scope as materialized (
    select
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      nullif(d.invoice_number, '') as invoice_number_raw,
      nullif(d.order_id, '') as order_id_raw,
      nullif(public.norm_code(d.invoice_number), '') as invoice_number_norm,
      nullif(public.norm_code(d.order_id), '') as order_id_norm,
      d.document_date_unix::bigint as document_date_unix,
      d.merchant_id,
      d.customer_name,
      nullif(public.norm_core_ns(coalesce(d.merchant_name, d.merchant_info::jsonb->>'name')), '') as doc_merchant_ns,
      nullif(public.norm_text_ns(
        concat_ws(' ',
          d.customer_name,
          d.customer_info::jsonb->>'name',
          d.customer_info::jsonb->>'contact_person',
          d.customer_info::jsonb->>'company_name',
          d.customer_info::jsonb->>'email'
        )
      ), '') as doc_customer_ns,
      nullif(public.norm_iban(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '') as doc_iban_ns,
      nullif(public.norm_iban_core(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '') as doc_iban_core_ns,
      coalesce(
        d.customer_name,
        d.customer_info::jsonb->>'name',
        d.customer_info::jsonb->>'contact_person',
        d.customer_info::jsonb->>'company_name',
        d.customer_info::jsonb->>'email'
      ) as doc_customer_display,
      coalesce(nullif(d.dedup_serialized,''), d.document_id::text) as doc_dedup_key
    from public.documents_v2 d
    where (p_company_id is null or d.company_id = p_company_id)
      and d.reconciled is not true
  ),

  transactions_scope as materialized (
    select
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.invoice_nr,''),
            (
              select match[1]
              from regexp_matches(
                upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
                '(?:\y(?:INV(?:OICE)?|FACTUUR(?:NR|NUMMER)?)\y\s*[-:#_/]*)?((?:\d{6}[-/]\d{2,20}|\d{4}[-/]\d{2,20}|[A-Z]{1,10}[-/]\d{2,20}|[A-Z0-9]{6,}))',
                'g'
              ) as match
              limit 1
            )
          )
        ),
      '') as invoice_nr_norm,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.order_id,''),
            (
              select match[1]
              from regexp_matches(
                upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
                '(?:\y(?:ORD(?:ER)?|PO|ORDER)\y\s*[-:#_/]*)?((?:\d{2,20}|[A-Z]{1,10}[-/]\d{2,20}))',
                'g'
              ) as match
              limit 1
            )
          )
        ),
      '') as order_id_norm,
      t.completed_date::bigint as completed_date,
      t.merchant_id,
      t.name_consumer,
      (nullif(t.credited,0) is not null or nullif(t.money_in,0) is not null) as tx_is_in,
      (nullif(t.debited,0) is not null or nullif(t.money_out,0) is not null) as tx_is_out,
      nullif(public.norm_core_ns(coalesce(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)), '') as tx_merchant_ns,
      nullif(public.norm_text_ns(coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty)), '') as tx_customer_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '') as tx_iban_beneficiary_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '') as tx_iban_counterparty_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '') as tx_iban_beneficiary_core_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '') as tx_iban_counterparty_core_ns,
      coalesce(t.name_consumer, t.account_holder_name, t.account_holder_name_counterparty) as tx_customer_display,
      case
        when t.debited is not null and t.debited > 0 then t.debited
        when t.credited is not null and t.credited > 0 then t.credited
        when t.amount is not null then abs(t.amount)
        else null
      end as effective_amount
    from public.transactions t
    where (p_company_id is null or t.company_id = p_company_id::text)
      and t.reconciled is not true
      and (
           t.invoice_nr is not null
        or t.order_id is not null
        or (t.currency is not null and t.merchant_id is not null)
        or (t.currency is not null and (t.name_consumer is not null or t.account_holder_name is not null or t.account_holder_name_counterparty is not null))
        or (t.currency is not null and (t.amount is not null or t.debited is not null or t.credited is not null))
      )
    order by t.completed_date desc, t.transaction_id
    limit v_max_transactions_to_scan
  ),

  s1_invoice as (
    select t.transaction_id, d.document_id, 'invoice_number' as seed
    from transactions_scope t
    join document_scope d
      on d.company_id::text = t.company_id
     and d.invoice_number_norm is not null
     and t.invoice_nr_norm is not null
     and d.invoice_number_norm = t.invoice_nr_norm
     and d.currency = t.currency
  ),

  s2_order as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'order_number' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and d.order_id_norm is not null
       and t.order_id_norm is not null
       and d.order_id_norm = t.order_id_norm
       and d.currency = t.currency
    ) s
    left join (select distinct transaction_id from s1_invoice) u using (transaction_id)
    where u.transaction_id is null
  ),

  s3_iban_exact_amount_out as (
    select t.transaction_id, d_out.document_id, 'iban_exact_amount_out' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_order) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and t.debited is not null and t.debited > 0
        and d.total = t.debited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_out on true
    where done.transaction_id is null
  ),

  s3_iban_exact_amount_in as (
    select t.transaction_id, d_in.document_id, 'iban_exact_amount_in' as seed
    from transactions_scope t
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_order) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and t.credited is not null and t.credited > 0
        and d.total = t.credited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_in on true
    where done.transaction_id is null
  ),

  s3_iban_exact_amount as (
    select * from s3_iban_exact_amount_out
    union all
    select * from s3_iban_exact_amount_in
  ),

  s4_merchant_id_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_id_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.merchant_id = t.merchant_id
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (select distinct transaction_id from s1_invoice union all select distinct transaction_id from s2_order union all select distinct transaction_id from s3_iban_exact_amount) done using (transaction_id)
    where done.transaction_id is null
  ),

  s5_merchant_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
       and (d.doc_merchant_ns = t.tx_merchant_ns or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0 or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0)
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s6_customer_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'customer_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_in)
       and d.currency = t.currency
       and d.doc_customer_ns is not null and t.tx_customer_ns is not null
       and (d.doc_customer_ns = t.tx_customer_ns or position(d.doc_customer_ns in t.tx_customer_ns) > 0 or position(t.tx_customer_ns in d.doc_customer_ns) > 0)
       and t.credited is not null and t.credited > 0
       and d.total = t.credited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s7_iban_plus_signal as (
    select t.transaction_id, d_sub.document_id, 'iban_plus_signal' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and (
             (((not v_directional_matching) or t.tx_is_out) and ((d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns) or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns)))
          or (((not v_directional_matching) or t.tx_is_in)  and ((d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns) or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns)))
        )
        and (
             (t.invoice_nr_norm is not null and d.invoice_number_norm is not null)
          or (t.order_id_norm is not null and d.order_id_norm is not null)
          or (t.tx_merchant_ns is not null and d.doc_merchant_ns is not null)
          or (t.tx_customer_ns is not null and d.doc_customer_ns is not null)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_precise
    ) d_sub on true
    where done.transaction_id is null
  ),

  s8_merchant_id_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_id_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.merchant_id = t.merchant_id
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction)) and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s9_merchant_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
        and (d.doc_merchant_ns = t.tx_merchant_ns or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0 or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0)
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction)) and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s10_customer_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'customer_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and d.doc_customer_ns is not null and t.tx_customer_ns is not null
        and (d.doc_customer_ns = t.tx_customer_ns or position(d.doc_customer_ns in t.tx_customer_ns) > 0 or position(t.tx_customer_ns in d.doc_customer_ns) > 0)
        and v_amount_tolerance_fraction > 0
        and t.credited is not null and t.credited > 0
        and d.total between (t.credited / (1 + v_amount_tolerance_fraction)) and (t.credited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.credited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s11_iban_tol as (
    select t.transaction_id, d_sub.document_id, 'iban_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and (
          (
            ((not v_directional_matching) or t.tx_is_out)
            and (
              (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns)
            )
            and t.debited is not null and t.debited > 0
            and d.total between (t.debited / (1 + v_amount_tolerance_fraction)) and (t.debited / (1 - v_amount_tolerance_fraction))
          )
          or
          (
            ((not v_directional_matching) or t.tx_is_in)
            and (
              (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns)
            )
            and t.credited is not null and t.credited > 0
            and d.total between (t.credited / (1 + v_amount_tolerance_fraction)) and (t.credited / (1 - v_amount_tolerance_fraction))
          )
        )
      order by coalesce(abs(d.total - t.debited), abs(d.total - t.credited)), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s12_window_amount_exact as (
    select t.transaction_id, d_sub.document_id, 'window_amount_exact' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and d.total = coalesce(nullif(t.debited,0), nullif(t.credited,0), abs(t.amount))
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s13_window_amount_tol as (
    select t.transaction_id, d_sub.document_id, 'window_amount_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and d.total between (coalesce(nullif(t.debited,0), nullif(t.credited,0), abs(t.amount)) / (1 + v_amount_tolerance_fraction)) and (coalesce(nullif(t.debited,0), nullif(t.credited,0), abs(t.amount)) / (1 - v_amount_tolerance_fraction))
      order by abs(d.total - coalesce(nullif(t.debited,0), nullif(t.credited,0), abs(t.amount))), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s14_window_only as (
    select t.transaction_id, d_sub.document_id, 'window_only' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
      union all select distinct transaction_id from s13_window_amount_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_window_only
    ) d_sub on true
    where done.transaction_id is null
  ),

  seeds as (
    select * from s1_invoice
    union all select * from s2_order
    union all select * from s3_iban_exact_amount
    union all select * from s4_merchant_id_exact_amount
    union all select * from s5_merchant_ns_exact_amount
    union all select * from s6_customer_ns_exact_amount
    union all select * from s7_iban_plus_signal
    union all select * from s8_merchant_id_tol
    union all select * from s9_merchant_ns_tol
    union all select * from s10_customer_ns_tol
    union all select * from s11_iban_tol
    union all select * from s12_window_amount_exact
    union all select * from s13_window_amount_tol
    union all select * from s14_window_only
  ),

  seed_counts_total as (
    select seed, count(*) as ct from seeds group by seed
  ),
  seed_counts_distinct as (
    select seed, count(distinct (transaction_id, document_id)) as ct from seeds group by seed
  ),

  candidate_pairs as (
    select distinct transaction_id, document_id from seeds
  ),

  distinct_pairs as (
    select distinct transaction_id, document_id from candidate_pairs
  ),

  attributes as (
    select
      p.transaction_id,
      p.document_id,
      t.company_id::uuid as transaction_company_id,
      d.company_id as document_company_id,
      t.amount as transaction_amount,
      t.debited as transaction_debited,
      t.credited as transaction_credited,
      d.total as document_total,
      t.currency as transaction_currency,
      d.currency as document_currency,
      t.invoice_nr_norm as tx_invoice_norm,
      d.invoice_number_norm as doc_invoice_norm,
      t.order_id_norm as tx_order_norm,
      d.order_id_norm as doc_order_norm,
      d.invoice_number_raw as doc_invoice_raw,
      d.order_id_raw as doc_order_raw,
      t.completed_date as transaction_date_unix,
      d.document_date_unix as document_date_unix,
      t.merchant_id as transaction_merchant_id,
      d.merchant_id as document_merchant_id,
      t.tx_customer_display as transaction_customer_name,
      d.doc_customer_display as document_customer_name,
      t.tx_is_in, t.tx_is_out,
      t.tx_merchant_ns, d.doc_merchant_ns,
      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_iban_beneficiary_ns,
      t.tx_iban_counterparty_ns,
      d.doc_iban_ns,
      t.tx_iban_beneficiary_core_ns,
      t.tx_iban_counterparty_core_ns,
      d.doc_iban_core_ns,
      coalesce(nullif(d.doc_dedup_key,''), d.document_id::text) as doc_dedup_key,
      t.effective_amount as transaction_effective_amount
    from distinct_pairs p
    join transactions_scope t on t.transaction_id = p.transaction_id
    join document_scope d on d.document_id = p.document_id
    where d.company_id::text = t.company_id
  ),

  evaluated as (
    select
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,
      coalesce((a.tx_invoice_norm is not null and a.doc_invoice_norm is not null and a.tx_invoice_norm = a.doc_invoice_norm), false) as invoice_number_match,
      coalesce((a.tx_order_norm is not null and a.doc_order_norm is not null and a.tx_order_norm = a.doc_order_norm), false) as order_number_match,
      coalesce(a.transaction_currency = a.document_currency, false) as currency_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_out and (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (a.tx_merchant_ns = a.doc_merchant_ns or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or ((a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns) or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns))
            ))
          else
            (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (a.tx_merchant_ns = a.doc_merchant_ns or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or ((a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns) or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns))
            )
        end
      ) as merchant_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_in and (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (a.tx_customer_ns = a.doc_customer_ns or position(a.doc_customer_ns in a.tx_customer_ns) > 0 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or ((a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns) or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns))
            ))
          else
            (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (a.tx_customer_ns = a.doc_customer_ns or position(a.doc_customer_ns in a.tx_customer_ns) > 0 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or ((a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns) or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns))
            )
        end
      ) as customer_match,
      coalesce(abs(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, false) as date_within_cap,
      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,
      (a.transaction_debited is not null and a.transaction_debited > 0 and a.document_total = a.transaction_debited) as amount_exact_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and a.document_total = a.transaction_credited) as amount_exact_from_credited,
      (a.transaction_debited is not null and a.transaction_debited > 0 and abs(a.document_total - a.transaction_debited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and abs(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_credited,
      (a.transaction_effective_amount is not null and a.document_total = a.transaction_effective_amount) as amount_exact_from_effective,
      (a.transaction_effective_amount is not null and abs(a.document_total - a.transaction_effective_amount) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_effective,
      a.tx_is_in, a.tx_is_out,
      a.tx_iban_beneficiary_ns,
      a.tx_iban_counterparty_ns,
      a.doc_iban_ns,
      a.tx_iban_beneficiary_core_ns,
      a.tx_iban_counterparty_core_ns,
      a.doc_iban_core_ns,
      a.tx_merchant_ns,
      a.doc_merchant_ns,
      a.tx_customer_ns,
      a.doc_customer_ns,
      a.transaction_merchant_id,
      a.document_merchant_id
    from attributes a
  ),

  with_amount_tolerance as (
    select
      e.*,
      case
        when v_directional_matching and e.tx_is_out then e.amount_exact_from_debited
        when v_directional_matching and e.tx_is_in then e.amount_exact_from_credited
        else coalesce(e.amount_exact_from_debited,false) or coalesce(e.amount_exact_from_credited,false) or coalesce(e.amount_exact_from_effective,false)
      end as amount_exact_match,
      case
        when v_amount_tolerance_fraction > 0 then
          case
            when v_directional_matching and e.tx_is_out then e.amount_tolerance_from_debited
            when v_directional_matching and e.tx_is_in then e.amount_tolerance_from_credited
            else coalesce(e.amount_tolerance_from_debited,false) or coalesce(e.amount_tolerance_from_credited,false) or coalesce(e.amount_tolerance_from_effective,false)
          end
        else false
      end as amount_tolerance_match,
      (coalesce(e.amount_exact_from_debited,false) or coalesce(e.amount_exact_from_credited,false) or coalesce(e.amount_tolerance_from_debited,false) or coalesce(e.amount_tolerance_from_credited,false) or coalesce(e.amount_exact_from_effective,false) or coalesce(e.amount_tolerance_from_effective,false)) as amount_direction_ok
    from evaluated e
  ),

  resolved_weights as (
    select
      coalesce((v_weights_json->>'amount_total')::numeric, 0) as amount_weight,
      coalesce((v_weights_json->>'invoice_number')::numeric, 0) as invoice_weight,
      coalesce((v_weights_json->>'order_number')::numeric, 0) as order_weight,
      coalesce((v_weights_json->>'date')::numeric, 0) as date_weight,
      coalesce((v_weights_json->>'currency')::numeric, 0) as currency_weight,
      coalesce((v_weights_json->>'merchant_id')::numeric, 0) as merchant_weight,
      coalesce((v_weights_json->>'customer_name')::numeric, 0) as customer_weight
  ),

  scored_components as (
    select
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,
      m.transaction_date_unix,
      m.document_date_unix,
      m.amount_direction_ok,
      m.tx_is_in, m.tx_is_out,
      m.tx_iban_beneficiary_ns,
      m.tx_iban_counterparty_ns,
      m.doc_iban_ns,
      m.tx_iban_beneficiary_core_ns,
      m.tx_iban_counterparty_core_ns,
      m.doc_iban_core_ns,
      m.tx_merchant_ns,
      m.doc_merchant_ns,
      m.tx_customer_ns,
      m.doc_customer_ns,
      m.transaction_merchant_id,
      m.document_merchant_id,
      coalesce(
        case
          when m.amount_exact_match then w.amount_weight
          when m.amount_tolerance_match then w.amount_weight * exp(-0.5 * power((abs(m.transaction_effective_amount - m.document_total)) / nullif((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0), 2))
          else 0
        end, 0
      )::numeric as amount_score,
      (m.amount_exact_match or m.amount_tolerance_match) as amount_match,
      m.invoice_number_match as invoice_number_match,
      m.order_number_match as order_number_match,
      case
        when m.transaction_date_unix is not null and m.document_date_unix is not null then w.date_weight * exp(-0.5 * power(greatest(0,(abs(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days) / nullif(greatest(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k), 0), 2))
        else 0
      end::numeric as date_score,
      m.date_within_cap as date_match,
      m.currency_match as currency_match,
      m.merchant_match as merchant_match,
      m.customer_match as customer_match,
      w.amount_weight as amount_weight,
      w.invoice_weight as invoice_weight,
      w.order_weight as order_weight,
      w.date_weight as date_weight,
      w.currency_weight as currency_weight,
      w.merchant_weight as merchant_weight,
      w.customer_weight as customer_weight,
      (coalesce(m.invoice_number_match::int,0) * w.invoice_weight)::numeric as invoice_score,
      (coalesce(m.order_number_match::int,0) * w.order_weight)::numeric as order_score,
      (coalesce(m.currency_match::int,0) * w.currency_weight)::numeric as currency_score,
      (coalesce(m.merchant_match::int,0) * w.merchant_weight)::numeric as merchant_score,
      (coalesce(m.customer_match::int,0) * w.customer_weight)::numeric as customer_score
    from with_amount_tolerance m
    cross join resolved_weights w
  ),

  scored_with_applicability as (
    select
      s.*,
      (
        case
          when v_directional_matching then
            ( s.tx_is_out and ((s.doc_merchant_ns is not null and s.tx_merchant_ns is not null) or ((s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns) or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns)) or (s.document_merchant_id is not null and s.transaction_merchant_id is not null)) )
          else
            ( (s.doc_merchant_ns is not null and s.tx_merchant_ns is not null) or ((s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns) or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns)) or (s.document_merchant_id is not null and s.transaction_merchant_id is not null) )
        end
      ) as merchant_applicable,
      (
        case
          when v_directional_matching then
            ( s.tx_is_in and ((s.doc_customer_ns is not null and s.tx_customer_ns is not null) or ((s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns) or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns)) ) )
          else
            ( (s.doc_customer_ns is not null and s.tx_customer_ns is not null) or ((s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns) or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns)) )
        end
      ) as customer_applicable
    from scored_components s
  ),

  scored_with_confidence as (
    select
      s.*,
      (
        s.amount_weight + s.invoice_weight + s.order_weight
        + (case when s.transaction_date_unix is not null and s.document_date_unix is not null then s.date_weight else 0 end)
        + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      )::numeric as dynamic_total_weight,
      (
        s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
        + (case when s.merchant_applicable then s.merchant_score else 0 end)
        + (case when s.customer_applicable then s.customer_score else 0 end)
      )::numeric as total_numerator,
      (coalesce(s.amount_match::int,0) + coalesce(s.invoice_number_match::int,0) + coalesce(s.order_number_match::int,0) + coalesce(s.date_match::int,0) + coalesce(s.currency_match::int,0) + coalesce(s.merchant_match::int,0) + coalesce(s.customer_match::int,0)) as signals_matched_count,
      case when (
        s.amount_weight + s.invoice_weight + s.order_weight
        + (case when s.transaction_date_unix is not null and s.document_date_unix is not null then s.date_weight else 0 end)
        + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      ) > 0 then
        round(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + (case when s.merchant_applicable then s.merchant_score else 0 end) + (case when s.customer_applicable then s.customer_score else 0 end)) / (s.amount_weight + s.invoice_weight + s.order_weight + (case when s.transaction_date_unix is not null and s.document_date_unix is not null then s.date_weight else 0 end) + s.currency_weight + (case when s.merchant_applicable then s.merchant_weight else 0 end) + (case when s.customer_applicable then s.customer_weight else 0 end))) * 100.0, 4)
      else 0 end as confidence_percent
    from scored_with_applicability s
  ),

  low_signal as (select * from scored_with_confidence where signals_matched_count < 1),
  below_min_confidence as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent < v_config_min_confidence),
  base_candidates as (select * from scored_with_confidence where signals_matched_count >= 1 and confidence_percent >= v_config_min_confidence),

  ranked_per_transaction as (
    select b.*, row_number() over (partition by b.transaction_id order by b.confidence_percent desc, b.total_numerator desc, b.signals_matched_count desc, b.document_id) as rownum_per_transaction
    from base_candidates b
  ),
  not_top_for_transaction as (select * from ranked_per_transaction where rownum_per_transaction > 1),
  top_per_transaction as (select * from ranked_per_transaction where rownum_per_transaction = 1),

  ranked_per_document as (
    select t.*, row_number() over (partition by t.doc_dedup_key order by t.confidence_percent desc, t.total_numerator desc, t.signals_matched_count desc, abs(t.transaction_date_unix - t.document_date_unix) asc, t.document_id) as rownum_per_document
    from top_per_transaction t
  ),
  not_top_for_document as (select * from ranked_per_document where rownum_per_document > 1),
  top_per_document as (select * from ranked_per_document where rownum_per_document = 1),

  final_candidates as (
    select * from top_per_document
    order by confidence_percent desc, total_numerator desc, signals_matched_count desc, transaction_id, document_id
  ),

  allowed_for_insert as (
    select f.*
    from final_candidates f
    where not exists (select 1 from public.reconciliations r1 where r1.transaction_id = f.transaction_id)
      and not exists (select 1 from public.reconciliations r2 where r2.document_id = f.document_id)
  ),

  inserted_reconciliations as (
    insert into public.reconciliations (
      transaction_id, document_id, company_id, confidence, time, engine_version, fiscal_year, quarter, month, summary, note
    )
    with ts_base as (
      select f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent, coalesce(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint as base_unix
      from allowed_for_insert f
    ),
    enrich as (
      select
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match as invoice_match,
        d.order_number_match as order_match,
        d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        d.amount_score, d.amount_weight,
        d.date_score, d.date_weight,
        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm, a.tx_order_norm, a.doc_order_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw, a.doc_order_raw,
        a.tx_iban_beneficiary_ns, a.tx_iban_counterparty_ns, a.doc_iban_ns,
        a.tx_iban_beneficiary_core_ns, a.tx_iban_counterparty_core_ns, a.doc_iban_core_ns,
        a.tx_is_in, a.tx_is_out
      from ts_base t
      join final_candidates d on d.transaction_id = t.transaction_id and d.document_id = t.document_id
      join attributes a on a.transaction_id = d.transaction_id and a.document_id = d.document_id
    ),
    notes as (
      select
        e.*,
        case when e.document_total is not null and e.document_total <> 0 then round(abs(e.transaction_effective_amount - e.document_total) / nullif(e.document_total,0) * 100.0, 2) else null end as amt_diff_pct,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) else null end as sec_diff,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) / 86400.0 else null end as day_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) / 3600.0 else null end as hour_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null then abs(e.transaction_date_unix - e.document_date_unix) / 60.0 else null end as minute_gap,
        case when e.amount_weight > 0 then round((e.amount_score / e.amount_weight) * 100.0, 2) end as amount_ratio_pct,
        case when e.date_weight > 0 then round((e.date_score / e.date_weight) * 100.0, 0) end as date_ratio_pct
      from enrich e
    ),
    notes_text as (
      select
        n.*,
        case
          when n.date_weight > 0 and n.date_ratio_pct <= 99 then
            trim(both ' ' from concat_ws(' ',
              case when n.amount_weight > 0 and n.amount_score < n.amount_weight and n.amt_diff_pct is not null then concat('Amount scored ', coalesce(n.amount_ratio_pct::text,'0'), '% because payment vs document differed by ', n.amt_diff_pct::text, '%.') end,
              case when n.day_gap is not null then concat('Date scored ', coalesce(n.date_ratio_pct::text,'0'), '% because dates were ', round(n.day_gap::numeric, 2)::text, ' days apart.') end
            ))
          else null
        end as note_text
      from notes n
    )
    select
      e.transaction_id, e.document_id, e.document_company_id, coalesce(e.confidence_percent, 0)::numeric, v_log_run_unix_time, v_engine_version,
      extract(year from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      extract(quarter from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) at time zone 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (select json_agg(s) from unnest(array[
          case when e.invoice_match then 'invoice' end,
          case when e.amount_match then 'amount' end,
          case when e.date_match then 'date' end,
          case when e.currency_match then 'currency' end,
          case when e.merchant_match then 'merchant' end,
          case when e.customer_match then 'customer' end,
          case when e.order_match then 'order' end
        ]) s where s is not null),
        'tx_amount', e.transaction_effective_amount,
        'doc_total', e.document_total,
        'tx_invoice', e.tx_invoice_norm,
        'doc_invoice', coalesce(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_order', e.tx_order_norm,
        'doc_order', coalesce(e.doc_order_raw, e.doc_order_norm),
        'tx_currency', e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant', e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer', e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix', e.transaction_date_unix,
        'doc_unix', e.document_date_unix,
        'tx_iban_beneficiary', e.tx_iban_beneficiary_ns,
        'tx_iban_counterparty', e.tx_iban_counterparty_ns,
        'doc_iban', e.doc_iban_ns,
        'tx_iban_beneficiary_core', e.tx_iban_beneficiary_core_ns,
        'tx_iban_counterparty_core', e.tx_iban_counterparty_core_ns,
        'doc_iban_core', e.doc_iban_core_ns,
        'tx_is_in', e.tx_is_in,
        'tx_is_out', e.tx_is_out,
        'direction', case when e.tx_is_in then 'incoming' when e.tx_is_out then 'outgoing' else 'unknown' end,
        'days_between_tx_and_doc', case when e.transaction_date_unix is not null and e.document_date_unix is not null then (abs(e.transaction_date_unix - e.document_date_unix) / 86400)::int else null end
      )::text,
      nullif(nt.note_text,'')
    from notes_text nt
    join enrich e using (transaction_id, document_id)
    returning reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores as (
    insert into public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
    )
    select
      r.reconciliation_id,
      d.document_company_id as company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      d.order_number_match,
      d.merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      d.order_weight,
      d.merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      d.order_score,
      d.merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    from inserted_reconciliations r
    join scored_with_confidence d on d.transaction_id = r.transaction_id and d.document_id = r.document_id
    returning 1
  ),

  final_counts as (
    select
      (select count(*) from top_per_transaction) as top_per_transaction_count,
      (select count(*) from top_per_document) as top_per_document_count,
      (select count(*) from final_candidates) as passed_confidence_count,
      (select count(*) from inserted_reconciliations) as inserted_count,
      (select count(*) from base_candidates) as candidates_count,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_total) as seed_counts_total_json,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_distinct) as seed_counts_distinct_json,
      (select count(*) from transactions_scope) as total_tx_count,
      (select count(*) from document_scope) as total_doc_count
  )
  select
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  into
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  from final_counts;

  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message, company_id, transaction_id, document_id, reconciliation_id, rows_affected, confidence, step_ms, processing_ms, context
  ) values (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', null, p_company_id, null, null, null, v_inserted_count, null, null, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered', v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic', v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  return jsonb_build_object(
    'msg', v_status_message,
    'time_ms', v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered', v_total_docs,
    'top_per_tx', v_summary_top_per_tx,
    'top_per_doc', v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic', v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

exception when others then
  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
  get stacked diagnostics v_err_state = returned_sqlstate, v_err_msg = message_text, v_err_detail = pg_exception_detail, v_err_hint = pg_exception_hint, v_err_context = pg_exception_context;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message, company_id, transaction_id, document_id, reconciliation_id, rows_affected, confidence, step_ms, processing_ms, context
  ) values (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg, p_company_id, null, null, null, null, null, null, v_elapsed_ms,
    jsonb_build_object('sqlstate', v_err_state, 'detail', v_err_detail, 'hint', v_err_hint, 'context', v_err_context)
  );

  return jsonb_build_object('msg','error','time_ms',v_elapsed_ms,'transactions_scanned',0,'documents_considered',0,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,'error',v_err_msg,'sqlstate',v_err_state);
end;
$$;

alter function athena_reconcile_general_latest(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_latest(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_latest(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_latest(unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v1_9_p(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
declare
  v_status_message text := 'success';
  v_engine_version text := '1.9P';
  v_start_time timestamptz := clock_timestamp();
  v_elapsed_ms bigint := 0;

  v_config_amount_tolerance           numeric;
  v_config_date_window_days           numeric;
  v_config_min_confidence             numeric;
  v_config_allow_document_duplicates  boolean;
  v_config_log_activity               boolean;

  v_amount_tolerance_fraction numeric;
  v_date_window_seconds       bigint;
  v_date_full_weight_grace_days int := 30;

  v_edge_weight_small constant numeric := 0.10;
  v_gauss_k            constant numeric := sqrt(2 * ln(1.0 / v_edge_weight_small));

  v_cap_precise               int := 5;
  v_cap_broad                 int := 10;
  v_cap_window_only           int := 3;
  v_cap_iban_exact            int := 5;
  v_iban_exact_window_seconds bigint;
  v_max_transactions_to_scan  int := 20000;

  v_weights_json           jsonb := '{}'::jsonb;
  v_weights_total_possible numeric := 0;

  v_summary_top_per_tx   bigint := 0;
  v_summary_top_per_doc  bigint := 0;
  v_summary_passed_count bigint := 0;
  v_inserted_count       bigint := 0;
  v_candidates_count     bigint := 0;

  v_total_tx   bigint := 0;
  v_total_docs bigint := 0;

  v_seed_counts_total    jsonb := '{}'::jsonb;
  v_seed_counts_distinct jsonb := '{}'::jsonb;

  v_log_run_id        uuid := gen_random_uuid();
  v_log_run_unix_time bigint := extract(epoch from now())::bigint;

  v_directional_matching boolean := true;

  v_err_state   text;
  v_err_msg     text;
  v_err_detail  text;
  v_err_hint    text;
  v_err_context text;
begin
  select amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  into   v_config_amount_tolerance, v_config_date_window_days, v_config_min_confidence,
         v_config_allow_document_duplicates, v_config_log_activity
  from   public.reconciliations_config
  where  reconciliation_config_id = p_reconciliation_config_id;

  if not found then
    v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
    return jsonb_build_object('msg','reconciliations_config not found','time_ms',v_elapsed_ms,
                              'transactions_scanned',0,'documents_considered',0,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  end if;

  v_amount_tolerance_fraction := case
    when v_config_amount_tolerance >= 1 then v_config_amount_tolerance / 100.0
    else v_config_amount_tolerance
  end;

  v_date_window_seconds := ceil(coalesce(v_config_date_window_days, 1) * 86400)::bigint;
  v_iban_exact_window_seconds := least(v_date_window_seconds, 14 * 86400)::bigint;

  if v_config_min_confidence is null then v_config_min_confidence := 0; end if;
  if v_config_min_confidence <= 1 then v_config_min_confidence := v_config_min_confidence * 100.0; end if;

  with candidate_weights as (
    select lower(trim(w.weight_key)) as k_norm,
           (w.weight_value)::numeric as v_norm,
           w.company_id
    from public.reconciliations_weight_registry w
    where w.active
      and w.merchant_id is null
      and (
           (p_company_id is null and w.company_id is null)
        or (p_company_id is not null and (w.company_id = p_company_id or w.company_id is null))
      )
      and lower(trim(w.weight_key)) in (
        'amount_total','invoice_number','order_number','date','currency','merchant_id','customer_name'
      )
  ),
  picked as (
    select distinct on (k_norm) k_norm, v_norm
    from candidate_weights
    order by k_norm, (company_id is null)
  )
  select coalesce(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) into v_weights_json from picked;

  select coalesce((v_weights_json->>'amount_total')::numeric,0)
       + coalesce((v_weights_json->>'invoice_number')::numeric,0)
       + coalesce((v_weights_json->>'order_number')::numeric,0)
       + coalesce((v_weights_json->>'date')::numeric,0)
       + coalesce((v_weights_json->>'currency')::numeric,0)
       + coalesce((v_weights_json->>'merchant_id')::numeric,0)
       + coalesce((v_weights_json->>'customer_name')::numeric,0)
  into v_weights_total_possible;

  if v_weights_total_possible = 0 then
    v_weights_json := jsonb_build_object(
      'amount_total',1,'invoice_number',1,'order_number',1,'date',1,'currency',1,'merchant_id',1,'customer_name',1
    );
    v_weights_total_possible := 7;
  end if;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_start', 'function', 'ok', null,
    p_company_id, null, null, null,
    null, null, null, null,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', v_config_log_activity,
        'allow_doc_dups', v_config_allow_document_duplicates,
        'amount_tol_pct', v_amount_tolerance_fraction,
        'min_confidence', v_config_min_confidence,
        'date_window_days', v_config_date_window_days,
        'max_tx_seed', v_max_transactions_to_scan,
        'cap_precise', v_cap_precise,
        'cap_broad',   v_cap_broad,
        'cap_window_only', v_cap_window_only,
        'cap_iban_exact', v_cap_iban_exact,
        'iban_exact_window_days', round(v_iban_exact_window_seconds/86400.0,2),
        'directional_matching', v_directional_matching,
        'gauss_edge_weight', v_edge_weight_small
      ),
      'weights_json', v_weights_json,
      'weights_max_possible', v_weights_total_possible
    )
  );

  with
  document_scope as materialized (
    select
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      nullif(d.invoice_number, '') as invoice_number_raw,
      nullif(d.order_id, '')       as order_id_raw,
      nullif(public.norm_code(d.invoice_number), '') as invoice_number_norm,
      nullif(public.norm_code(d.order_id), '')       as order_id_norm,
      d.document_date_unix::bigint as document_date_unix,
      d.merchant_id,
      d.customer_name,
      nullif(public.norm_core_ns(coalesce(d.merchant_name, d.merchant_info::jsonb->>'name')), '') as doc_merchant_ns,
      nullif(public.norm_text_ns(
        concat_ws(' ',
          d.customer_name,
          d.customer_info::jsonb->>'name',
          d.customer_info::jsonb->>'contact_person',
          d.customer_info::jsonb->>'company_name',
          d.customer_info::jsonb->>'email'
        )
      ), '') as doc_customer_ns,
      nullif(public.norm_iban(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '')      as doc_iban_ns,
      nullif(public.norm_iban_core(replace(coalesce(d.merchant_info::jsonb->>'iban',''), 'O','0')), '') as doc_iban_core_ns,
      coalesce(
        d.customer_name,
        d.customer_info::jsonb->>'name',
        d.customer_info::jsonb->>'contact_person',
        d.customer_info::jsonb->>'company_name',
        d.customer_info::jsonb->>'email'
      ) as doc_customer_display,
      coalesce(nullif(d.dedup_serialized,''), d.document_id::text) as doc_dedup_key
    from public.documents_v2 d
    where (p_company_id is null or d.company_id = p_company_id)
      and d.reconciled is not true
  ),

  transactions_scope as materialized (
    select
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.invoice_nr,''),
            (select m[1]
             from regexp_matches(
               upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
               '(INV(?:OICE)?[ -_/]?\d{1,20}|FACTUUR[ -_/]?\d{1,20}|\d{6}[-/]\d{2,20}|[A-Z]{1,10}[-/]\d{2,20}|\d{4}[-/]\d{2,20})',
               'g'
             ) as m
             limit 1)
          )
        ),
      '') as invoice_nr_norm,
      nullif(
        public.norm_code(
          coalesce(
            nullif(t.order_id,''),
            (select m[1]
             from regexp_matches(
               upper(concat_ws(' ', t.remittance_information, t.description, t.transfer_reference, t.processed_description)),
               '(ORD(?:ER)?[ -_/]?\d{2,20}|PO[ -_/]?\d{2,20}|ORDER[ -_/]?\d{2,20})',
               'g'
             ) as m
             limit 1)
          )
        ),
      '') as order_id_norm,
      t.completed_date::bigint as completed_date,
      t.merchant_id,
      t.name_consumer,
      (nullif(t.credited,0) is not null or nullif(t.money_in,0)  is not null)  as tx_is_in,
      (nullif(t.debited,0)  is not null or nullif(t.money_out,0) is not null) as tx_is_out,
      nullif(public.norm_core_ns(
        coalesce(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
      ), '') as tx_merchant_ns,
      nullif(public.norm_text_ns(
        coalesce(
          t.name_consumer,
          t.account_holder_name,
          t.account_holder_name_counterparty
        )
      ), '') as tx_customer_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '')      as tx_iban_beneficiary_ns,
      nullif(public.norm_iban(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '')             as tx_iban_counterparty_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_beneficiary, t.iban, ''), 'O','0')), '') as tx_iban_beneficiary_core_ns,
      nullif(public.norm_iban_core(replace(coalesce(t.iban_counterparty, ''), 'O','0')), '')        as tx_iban_counterparty_core_ns,
      coalesce(
        t.name_consumer,
        t.account_holder_name,
        t.account_holder_name_counterparty
      ) as tx_customer_display,
      case
        when t.debited  is not null and t.debited  > 0 then t.debited
        when t.credited is not null and t.credited > 0 then t.credited
        when t.amount   is not null                  then abs(t.amount)
        else null
      end as effective_amount
    from public.transactions t
    where (p_company_id is null or t.company_id = p_company_id::text)
      and t.reconciled is not true
      and (
           t.invoice_nr is not null
        or t.order_id   is not null
        or (t.currency is not null and t.merchant_id   is not null)
        or (t.currency is not null and (t.name_consumer is not null or t.account_holder_name is not null or t.account_holder_name_counterparty is not null))
        or (t.currency is not null and (t.amount is not null or t.debited is not null or t.credited is not null))
      )
    order by t.completed_date desc, t.transaction_id
    limit v_max_transactions_to_scan
  ),

  s1_invoice as (
    select t.transaction_id, d.document_id, 'invoice_number' as seed
    from transactions_scope t
    join document_scope d
      on d.company_id::text = t.company_id
     and d.invoice_number_norm is not null
     and t.invoice_nr_norm     is not null
     and d.invoice_number_norm = t.invoice_nr_norm
     and d.currency = t.currency
  ),

  s2_order as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'order_number' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and d.order_id_norm is not null
       and t.order_id_norm is not null
       and d.order_id_norm = t.order_id_norm
       and d.currency = t.currency
    ) s
    left join (select distinct transaction_id from s1_invoice) u using (transaction_id)
    where u.transaction_id is null
  ),

  s3_iban_exact_amount_out as (
    select t.transaction_id, d_sub.document_id, 'iban_exact_amount_out' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and t.debited is not null and t.debited > 0
        and d.total = t.debited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_sub on true
    where done.transaction_id is null
  ),
  s3_iban_exact_amount_in as (
    select t.transaction_id, d_sub.document_id, 'iban_exact_amount_in' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and t.credited is not null and t.credited > 0
        and d.total = t.credited
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_iban_exact_window_seconds
        and (
             (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
          or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_iban_exact
    ) d_sub on true
    where done.transaction_id is null
  ),
  s3_iban_exact_amount as (
    select * from s3_iban_exact_amount_out
    union all
    select * from s3_iban_exact_amount_in
  ),

  s4_merchant_id_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_id_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.merchant_id = t.merchant_id
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s5_merchant_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'merchant_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_out)
       and d.currency = t.currency
       and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
       and (
         d.doc_merchant_ns = t.tx_merchant_ns
         or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
         or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
       )
       and t.debited is not null and t.debited > 0
       and d.total = t.debited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s6_customer_ns_exact_amount as (
    select s.*
    from (
      select t.transaction_id, d.document_id, 'customer_ns_exact_amount' as seed
      from transactions_scope t
      join document_scope d
        on d.company_id::text = t.company_id
       and ((not v_directional_matching) or t.tx_is_in)
       and d.currency = t.currency
       and d.doc_customer_ns is not null and t.tx_customer_ns is not null
       and (
         d.doc_customer_ns = t.tx_customer_ns
         or position(d.doc_customer_ns in t.tx_customer_ns) > 0
         or position(t.tx_customer_ns in d.doc_customer_ns) > 0
       )
       and t.credited is not null and t.credited > 0
       and d.total = t.credited
       and d.document_date_unix is not null and t.completed_date is not null
       and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
    ) s
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
    ) done using (transaction_id)
    where done.transaction_id is null
  ),

  s7_iban_plus_signal as (
    select t.transaction_id, d_sub.document_id, 'iban_plus_signal' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and (
             ( ((not v_directional_matching) or t.tx_is_out) and
               ( (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
                 or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns) ) )
          or ( ((not v_directional_matching) or t.tx_is_in ) and
               ( (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
                 or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns) ) )
        )
        and (
             (t.invoice_nr_norm is not null and d.invoice_number_norm is not null)
          or (t.order_id_norm   is not null and d.order_id_norm   is not null)
          or (t.tx_merchant_ns  is not null and d.doc_merchant_ns is not null)
          or (t.tx_customer_ns  is not null and d.doc_customer_ns is not null)
        )
      order by abs(d.document_date_unix - t.completed_date), d.document_id
      limit v_cap_precise
    ) d_sub on true
    where done.transaction_id is null
  ),

  s8_merchant_id_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_id_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.merchant_id = t.merchant_id
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction))
                        and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s9_merchant_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'merchant_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_out)
        and d.currency = t.currency
        and d.doc_merchant_ns is not null and t.tx_merchant_ns is not null
        and (
          d.doc_merchant_ns = t.tx_merchant_ns
          or position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
          or position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
        )
        and v_amount_tolerance_fraction > 0
        and t.debited is not null and t.debited > 0
        and d.total between (t.debited / (1 + v_amount_tolerance_fraction))
                        and (t.debited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.debited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s10_customer_ns_tol as (
    select t.transaction_id, d_sub.document_id, 'customer_ns_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and ((not v_directional_matching) or t.tx_is_in)
        and d.currency = t.currency
        and d.doc_customer_ns is not null and t.tx_customer_ns is not null
        and (
          d.doc_customer_ns = t.tx_customer_ns
          or position(d.doc_customer_ns in t.tx_customer_ns) > 0
          or position(t.tx_customer_ns in d.doc_customer_ns) > 0
        )
        and v_amount_tolerance_fraction > 0
        and t.credited is not null and t.credited > 0
        and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                        and (t.credited / (1 - v_amount_tolerance_fraction))
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.total - t.credited), abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s11_iban_tol as (
    select t.transaction_id, d_sub.document_id, 'iban_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and
            ( (d.doc_iban_ns is not null and t.tx_iban_counterparty_ns is not null and d.doc_iban_ns = t.tx_iban_counterparty_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_counterparty_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_counterparty_core_ns) )
            and t.debited is not null and t.debited > 0
            and d.total between (t.debited  / (1 + v_amount_tolerance_fraction))
                            and (t.debited  / (1 - v_amount_tolerance_fraction)) )
          or
          ( ((not v_directional_matching) or t.tx_is_in) and
            ( (d.doc_iban_ns is not null and t.tx_iban_beneficiary_ns is not null and d.doc_iban_ns = t.tx_iban_beneficiary_ns)
              or (d.doc_iban_core_ns is not null and t.tx_iban_beneficiary_core_ns is not null and d.doc_iban_core_ns = t.tx_iban_beneficiary_core_ns) )
            and t.credited is not null and t.credited > 0
            and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                            and (t.credited / (1 - v_amount_tolerance_fraction)) )
        )
      order by
        coalesce(abs(d.total - t.debited), abs(d.total - t.credited)),
        abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s12_window_amount_exact as (
    select t.transaction_id, d_sub.document_id, 'window_amount_exact' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and t.debited  is not null and t.debited  > 0 and d.total = t.debited )
          or
          ( ((not v_directional_matching) or t.tx_is_in ) and t.credited is not null and t.credited > 0 and d.total = t.credited)
        )
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s13_window_amount_tol as (
    select t.transaction_id, d_sub.document_id, 'window_amount_tol' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
        and v_amount_tolerance_fraction > 0
        and (
          ( ((not v_directional_matching) or t.tx_is_out) and t.debited  is not null and t.debited  > 0
            and d.total between (t.debited  / (1 + v_amount_tolerance_fraction))
                            and (t.debited  / (1 - v_amount_tolerance_fraction)) )
          or
          ( ((not v_directional_matching) or t.tx_is_in ) and t.credited is not null and t.credited > 0
            and d.total between (t.credited / (1 + v_amount_tolerance_fraction))
                            and (t.credited / (1 - v_amount_tolerance_fraction)) )
        )
      order by
        coalesce(abs(d.total - t.debited), abs(d.total - t.credited)),
        abs(d.document_date_unix - t.completed_date)
      limit v_cap_broad
    ) d_sub on true
    where done.transaction_id is null
  ),

  s14_window_only as (
    select t.transaction_id, d_sub.document_id, 'window_only' as seed
    from transactions_scope t
    left join (
      select distinct transaction_id from s1_invoice
      union all select distinct transaction_id from s2_order
      union all select distinct transaction_id from s3_iban_exact_amount
      union all select distinct transaction_id from s4_merchant_id_exact_amount
      union all select distinct transaction_id from s5_merchant_ns_exact_amount
      union all select distinct transaction_id from s6_customer_ns_exact_amount
      union all select distinct transaction_id from s7_iban_plus_signal
      union all select distinct transaction_id from s8_merchant_id_tol
      union all select distinct transaction_id from s9_merchant_ns_tol
      union all select distinct transaction_id from s10_customer_ns_tol
      union all select distinct transaction_id from s11_iban_tol
      union all select distinct transaction_id from s12_window_amount_exact
      union all select distinct transaction_id from s13_window_amount_tol
    ) done using (transaction_id)
    join lateral (
      select d.document_id
      from document_scope d
      where d.company_id::text = t.company_id
        and d.currency = t.currency
        and d.document_date_unix is not null and t.completed_date is not null
        and abs(d.document_date_unix - t.completed_date) <= v_date_window_seconds
      order by abs(d.document_date_unix - t.completed_date)
      limit v_cap_window_only
    ) d_sub on true
    where done.transaction_id is null
  ),

  seeds as (
    select * from s1_invoice
    union all select * from s2_order
    union all select * from s3_iban_exact_amount
    union all select * from s4_merchant_id_exact_amount
    union all select * from s5_merchant_ns_exact_amount
    union all select * from s6_customer_ns_exact_amount
    union all select * from s7_iban_plus_signal
    union all select * from s8_merchant_id_tol
    union all select * from s9_merchant_ns_tol
    union all select * from s10_customer_ns_tol
    union all select * from s11_iban_tol
    union all select * from s12_window_amount_exact
    union all select * from s13_window_amount_tol
    union all select * from s14_window_only
  ),

  seed_counts_total as (
    select seed, count(*) as ct from seeds group by seed
  ),
  seed_counts_distinct as (
    select seed, count(distinct (transaction_id, document_id)) as ct from seeds group by seed
  ),

  candidate_pairs as (
    select distinct transaction_id, document_id from seeds
  ),

  distinct_pairs as (
    select distinct transaction_id, document_id
    from candidate_pairs
  ),

  attributes as (
    select
      p.transaction_id,
      p.document_id,
      t.company_id::uuid as transaction_company_id,
      d.company_id       as document_company_id,
      t.amount as transaction_amount,
      t.debited as transaction_debited,
      t.credited as transaction_credited,
      d.total  as document_total,
      t.currency as transaction_currency,
      d.currency as document_currency,
      t.invoice_nr_norm as tx_invoice_norm,
      d.invoice_number_norm as doc_invoice_norm,
      t.order_id_norm  as tx_order_norm,
      d.order_id_norm  as doc_order_norm,
      d.invoice_number_raw as doc_invoice_raw,
      d.order_id_raw       as doc_order_raw,
      t.completed_date as transaction_date_unix,
      d.document_date_unix as document_date_unix,
      t.merchant_id as transaction_merchant_id,
      d.merchant_id as document_merchant_id,
      t.tx_customer_display as transaction_customer_name,
      d.doc_customer_display as document_customer_name,
      t.tx_is_in, t.tx_is_out,
      t.tx_merchant_ns, d.doc_merchant_ns,
      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_iban_beneficiary_ns,
      t.tx_iban_counterparty_ns,
      d.doc_iban_ns,
      t.tx_iban_beneficiary_core_ns,
      t.tx_iban_counterparty_core_ns,
      d.doc_iban_core_ns,
      coalesce(nullif(d.doc_dedup_key,''), d.document_id::text) as doc_dedup_key,
      t.effective_amount as transaction_effective_amount
    from distinct_pairs p
    join transactions_scope t on t.transaction_id = p.transaction_id
    join document_scope    d on d.document_id    = p.document_id
    where d.company_id::text = t.company_id
  ),

  evaluated as (
    select
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,
      coalesce((a.tx_invoice_norm is not null and a.doc_invoice_norm is not null
                and a.tx_invoice_norm = a.doc_invoice_norm), false) as invoice_number_match,
      coalesce((a.tx_order_norm   is not null and a.doc_order_norm   is not null
                and a.tx_order_norm = a.doc_order_norm), false)     as order_number_match,
      coalesce(a.transaction_currency  = a.document_currency, false) as currency_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_out and (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null
                  and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or (
                   (a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns)
                or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            ))
          else
            (
                 (a.transaction_merchant_id is not null and a.document_merchant_id is not null
                  and a.transaction_merchant_id = a.document_merchant_id)
              or (a.tx_merchant_ns is not null and a.doc_merchant_ns is not null and (
                    a.tx_merchant_ns = a.doc_merchant_ns
                 or position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
                 or position(a.tx_merchant_ns in a.doc_merchant_ns) > 0))
              or (
                   (a.tx_iban_counterparty_ns is not null and a.doc_iban_ns is not null and a.tx_iban_counterparty_ns = a.doc_iban_ns)
                or (a.tx_iban_counterparty_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_counterparty_core_ns = a.doc_iban_core_ns)
                 )
            )
        end
      ) as merchant_match,
      (
        case
          when v_directional_matching then
            ( a.tx_is_in and (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (
                    a.tx_customer_ns = a.doc_customer_ns
                 or position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or (
                   (a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            ))
          else
            (
                 (a.tx_customer_ns is not null and a.doc_customer_ns is not null and (
                    a.tx_customer_ns = a.doc_customer_ns
                 or position(a.doc_customer_ns in a.tx_customer_ns) > 0
                 or position(a.tx_customer_ns in a.doc_customer_ns) > 0))
              or (
                   (a.tx_iban_beneficiary_ns is not null and a.doc_iban_ns is not null and a.tx_iban_beneficiary_ns = a.doc_iban_ns)
                or (a.tx_iban_beneficiary_core_ns is not null and a.doc_iban_core_ns is not null and a.tx_iban_beneficiary_core_ns = a.doc_iban_core_ns)
                 )
            )
        end
      ) as customer_match,
      coalesce(abs(a.transaction_date_unix - a.document_date_unix) <= v_date_window_seconds, false) as date_within_cap,
      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,
      a.doc_dedup_key,
      (a.transaction_debited  is not null and a.transaction_debited  > 0 and a.document_total = a.transaction_debited)  as amount_exact_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and a.document_total = a.transaction_credited) as amount_exact_from_credited,
      (a.transaction_debited  is not null and a.transaction_debited  > 0 and abs(a.document_total - a.transaction_debited)  <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_debited,
      (a.transaction_credited is not null and a.transaction_credited > 0 and abs(a.document_total - a.transaction_credited) <= a.document_total * v_amount_tolerance_fraction) as amount_tolerance_from_credited,
      a.tx_is_in, a.tx_is_out,
      a.tx_iban_beneficiary_ns,
      a.tx_iban_counterparty_ns,
      a.doc_iban_ns,
      a.tx_iban_beneficiary_core_ns,
      a.tx_iban_counterparty_core_ns,
      a.doc_iban_core_ns,
      a.tx_merchant_ns,
      a.doc_merchant_ns,
      a.tx_customer_ns,
      a.doc_customer_ns,
      a.transaction_merchant_id,
      a.document_merchant_id
    from attributes a
  ),

  with_amount_tolerance as (
    select
      e.*,
      case
        when e.tx_is_out and e.amount_exact_from_debited then true
        when e.tx_is_in  and e.amount_exact_from_credited then true
        when not v_directional_matching and (e.amount_exact_from_debited or e.amount_exact_from_credited) then true
        else false
      end as amount_exact_match,
      case
        when v_amount_tolerance_fraction > 0 then
          case
            when e.tx_is_out and e.amount_tolerance_from_debited then true
            when e.tx_is_in  and e.amount_tolerance_from_credited then true
            when not v_directional_matching and (e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) then true
            else false
          end
        else false
      end as amount_tolerance_match,
      (e.amount_exact_from_debited or e.amount_exact_from_credited or e.amount_tolerance_from_debited or e.amount_tolerance_from_credited) as amount_direction_ok
    from evaluated e
  ),

  resolved_weights as (
    select
      coalesce((v_weights_json->>'amount_total')::numeric, 0)   as amount_weight,
      coalesce((v_weights_json->>'invoice_number')::numeric, 0) as invoice_weight,
      coalesce((v_weights_json->>'order_number')::numeric, 0)   as order_weight,
      coalesce((v_weights_json->>'date')::numeric, 0)           as date_weight,
      coalesce((v_weights_json->>'currency')::numeric, 0)       as currency_weight,
      coalesce((v_weights_json->>'merchant_id')::numeric, 0)    as merchant_weight,
      coalesce((v_weights_json->>'customer_name')::numeric, 0)  as customer_weight
  ),

  scored_components as (
    select
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.doc_dedup_key,
      m.transaction_date_unix,
      m.document_date_unix,
      m.amount_direction_ok,
      m.tx_is_in, m.tx_is_out,
      m.tx_iban_beneficiary_ns,
      m.tx_iban_counterparty_ns,
      m.doc_iban_ns,
      m.tx_iban_beneficiary_core_ns,
      m.tx_iban_counterparty_core_ns,
      m.doc_iban_core_ns,
      m.tx_merchant_ns,
      m.doc_merchant_ns,
      m.tx_customer_ns,
      m.doc_customer_ns,
      m.transaction_merchant_id,
      m.document_merchant_id,
      coalesce(
        case
          when m.amount_exact_match then w.amount_weight
          when m.amount_tolerance_match then
            w.amount_weight * exp(
              -0.5 * power(
                (abs(m.transaction_effective_amount - m.document_total))
                / nullif((m.document_total * v_amount_tolerance_fraction) / v_gauss_k, 0)
              , 2)
            )
          else 0
        end, 0
      )::numeric as amount_score,
      (m.amount_exact_match or m.amount_tolerance_match) as amount_match,
      m.invoice_number_match  as invoice_number_match,
      m.order_number_match    as order_number_match,
      case
        when m.transaction_date_unix is not null and m.document_date_unix is not null then
          w.date_weight * exp(
            -0.5 * power(
              greatest(0,
                (abs(m.transaction_date_unix - m.document_date_unix) / 86400.0) - v_date_full_weight_grace_days
              ) / nullif( greatest(1.0, (v_config_date_window_days - v_date_full_weight_grace_days) / v_gauss_k ), 0)
            , 2)
          )
        else 0
      end::numeric as date_score,
      m.date_within_cap as date_match,
      m.currency_match  as currency_match,
      m.merchant_match  as merchant_match,
      m.customer_match  as customer_match,
      w.amount_weight   as amount_weight,
      w.invoice_weight  as invoice_weight,
      w.order_weight    as order_weight,
      w.date_weight     as date_weight,
      w.currency_weight as currency_weight,
      w.merchant_weight as merchant_weight,
      w.customer_weight as customer_weight,
      (coalesce(m.invoice_number_match::int,0)  * w.invoice_weight)::numeric  as invoice_score,
      (coalesce(m.order_number_match::int,0)    * w.order_weight)::numeric    as order_score,
      (coalesce(m.currency_match::int,0)        * w.currency_weight)::numeric as currency_score,
      (coalesce(m.merchant_match::int,0)        * w.merchant_weight)::numeric as merchant_score,
      (coalesce(m.customer_match::int,0)        * w.customer_weight)::numeric as customer_score
    from with_amount_tolerance m
    cross join resolved_weights w
  ),

  scored_with_applicability as (
    select
      s.*,
      (
        case
          when v_directional_matching then
            ( s.tx_is_out and (
                 (s.doc_merchant_ns is not null and s.tx_merchant_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              or (s.document_merchant_id is not null and s.transaction_merchant_id is not null)
            ))
          else
            (
                 (s.doc_merchant_ns is not null and s.tx_merchant_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_counterparty_ns is not null and s.doc_iban_ns = s.tx_iban_counterparty_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_counterparty_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_counterparty_core_ns) )
              or (s.document_merchant_id is not null and s.transaction_merchant_id is not null)
            )
        end
      ) as merchant_applicable,
      (
        case
          when v_directional_matching then
            ( s.tx_is_in and (
                 (s.doc_customer_ns is not null and s.tx_customer_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            ))
          else
            (
                 (s.doc_customer_ns is not null and s.tx_customer_ns is not null)
              or ( (s.doc_iban_ns is not null and s.tx_iban_beneficiary_ns is not null and s.doc_iban_ns = s.tx_iban_beneficiary_ns)
                   or (s.doc_iban_core_ns is not null and s.tx_iban_beneficiary_core_ns is not null and s.doc_iban_core_ns = s.tx_iban_beneficiary_core_ns) )
            )
        end
      ) as customer_applicable
    from scored_components s
  ),

  scored_with_confidence as (
    select
      s.*,
      (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      )::numeric as dynamic_total_weight,
      (
        s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
        + (case when s.merchant_applicable then s.merchant_score else 0 end)
        + (case when s.customer_applicable then s.customer_score else 0 end)
      )::numeric as total_numerator,
      (coalesce(s.amount_match::int,0)
     + coalesce(s.invoice_number_match::int,0)
     + coalesce(s.order_number_match::int,0)
     + coalesce(s.date_match::int,0)
     + coalesce(s.currency_match::int,0)
     + coalesce(s.merchant_match::int,0)
     + coalesce(s.customer_match::int,0)) as signals_matched_count,
      case when (
        s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
        + (case when s.merchant_applicable then s.merchant_weight else 0 end)
        + (case when s.customer_applicable then s.customer_weight else 0 end)
      ) > 0 then
        round(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score
              + (case when s.merchant_applicable then s.merchant_score else 0 end)
              + (case when s.customer_applicable then s.customer_score else 0 end))
            / (s.amount_weight + s.invoice_weight + s.order_weight + s.date_weight + s.currency_weight
              + (case when s.merchant_applicable then s.merchant_weight else 0 end)
              + (case when s.customer_applicable then s.customer_weight else 0 end))) * 100.0, 4)
      else 0 end as confidence_percent
    from scored_with_applicability s
  ),

  low_signal as (select * from scored_with_confidence where signals_matched_count < 1),
  below_min_confidence as (
    select * from scored_with_confidence
    where signals_matched_count >= 1 and confidence_percent < v_config_min_confidence
  ),
  base_candidates as (
    select * from scored_with_confidence
    where signals_matched_count >= 1 and confidence_percent >= v_config_min_confidence
  ),

  ranked_per_transaction as (
    select
      b.*,
      row_number() over (
        partition by b.transaction_id
        order by b.confidence_percent desc, b.total_numerator desc, b.signals_matched_count desc, b.document_id
      ) as rownum_per_transaction
    from base_candidates b
  ),
  not_top_for_transaction as (select * from ranked_per_transaction where rownum_per_transaction > 1),
  top_per_transaction as (select * from ranked_per_transaction where rownum_per_transaction = 1),

  ranked_per_document as (
    select
      t.*,
      row_number() over (
        partition by t.doc_dedup_key
        order by
          t.confidence_percent desc,
          t.total_numerator desc,
          t.signals_matched_count desc,
          abs(t.transaction_date_unix - t.document_date_unix) asc,
          t.document_id
      ) as rownum_per_document
    from top_per_transaction t
  ),
  not_top_for_document as (select * from ranked_per_document where rownum_per_document > 1),
  top_per_document as (select * from ranked_per_document where rownum_per_document = 1),

  final_candidates as (
    select *
    from top_per_document
    order by confidence_percent desc, total_numerator desc, signals_matched_count desc, transaction_id, document_id
  ),

  allowed_for_insert as (
    select f.*
    from   final_candidates f
    where  not exists (select 1 from public.reconciliations r1 where r1.transaction_id = f.transaction_id)
       and not exists (select 1 from public.reconciliations r2 where r2.document_id    = f.document_id)
  ),

  inserted_reconciliations as (
    insert into public.reconciliations (
      transaction_id, document_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary, note
    )
    with ts_base as (
      select f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             coalesce(f.transaction_date_unix, f.document_date_unix, v_log_run_unix_time)::bigint as base_unix
      from allowed_for_insert f
    ),
    enrich as (
      select
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_number_match as invoice_match,
        d.order_number_match   as order_match,
        d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        a.transaction_effective_amount,
        a.document_total,
        a.transaction_date_unix,
        a.document_date_unix,
        d.amount_score, d.amount_weight,
        d.date_score,   d.date_weight,
        t.base_unix,
        a.tx_invoice_norm, a.doc_invoice_norm, a.tx_order_norm, a.doc_order_norm,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name,
        a.doc_invoice_raw, a.doc_order_raw,
        a.tx_iban_beneficiary_ns, a.tx_iban_counterparty_ns, a.doc_iban_ns,
        a.tx_iban_beneficiary_core_ns, a.tx_iban_counterparty_core_ns, a.doc_iban_core_ns,
        a.tx_is_in, a.tx_is_out
      from ts_base t
      join final_candidates d on d.transaction_id = t.transaction_id and d.document_id = t.document_id
      join attributes a       on a.transaction_id = d.transaction_id and a.document_id = d.document_id
    ),
    notes as (
      select
        e.*,
        case
          when e.document_total is not null and e.document_total <> 0
          then round(abs(e.transaction_effective_amount - e.document_total) / nullif(e.document_total,0) * 100.0, 2)
          else null
        end as amt_diff_pct,
        case
          when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix)
          else null
        end as sec_diff,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 86400.0
          else null end as day_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 3600.0
          else null end as hour_gap,
        case when e.transaction_date_unix is not null and e.document_date_unix is not null
          then abs(e.transaction_date_unix - e.document_date_unix) / 60.0
          else null end as minute_gap,
        case when e.amount_weight > 0 then round((e.amount_score / e.amount_weight) * 100.0, 2) end as amount_ratio_pct,
        case when e.date_weight   > 0 then round((e.date_score   / e.date_weight)   * 100.0, 0)  end as date_ratio_pct
      from enrich e
    ),
    notes_text as (
      select
        n.*,
        case
          when n.date_weight > 0 and n.date_ratio_pct <= 99 then
            trim(both ' ' from concat_ws(' ',
              case
                when n.amount_weight > 0 and n.amount_score < n.amount_weight and n.amt_diff_pct is not null
                then concat('Amount scored ', coalesce(n.amount_ratio_pct::text,'0'),
                            '% because payment vs document differed by ', n.amt_diff_pct::text, '%.')
              end,
              case
                when n.day_gap is not null
                then concat('Date scored ', coalesce(n.date_ratio_pct::text,'0'),
                            '% because dates were ', round(n.day_gap::numeric, 2)::text, ' days apart.')
              end
            ))
          else null
        end as note_text
      from notes n
    )
    select
      e.transaction_id, e.document_id, e.document_company_id,
      coalesce(e.confidence_percent, 0)::numeric,
      v_log_run_unix_time, v_engine_version,
      extract(year from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      extract(quarter from (to_timestamp(e.base_unix) at time zone 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) at time zone 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          select json_agg(s) from unnest(array[
            case when e.invoice_match  then 'invoice'  end,
            case when e.amount_match   then 'amount'   end,
            case when e.date_match     then 'date'     end,
            case when e.currency_match then 'currency' end,
            case when e.merchant_match then 'merchant' end,
            case when e.customer_match then 'customer' end,
            case when e.order_match    then 'order'    end
          ]) s where s is not null
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.tx_invoice_norm,
        'doc_invoice',  coalesce(e.doc_invoice_raw, e.doc_invoice_norm),
        'tx_order',     e.tx_order_norm,
        'doc_order',    coalesce(e.doc_order_raw, e.doc_order_norm),
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix,
        'tx_iban_beneficiary',        e.tx_iban_beneficiary_ns,
        'tx_iban_counterparty',       e.tx_iban_counterparty_ns,
        'doc_iban',                   e.doc_iban_ns,
        'tx_iban_beneficiary_core',   e.tx_iban_beneficiary_core_ns,
        'tx_iban_counterparty_core',  e.tx_iban_counterparty_core_ns,
        'doc_iban_core',              e.doc_iban_core_ns,
        'tx_is_in',    e.tx_is_in,
        'tx_is_out',   e.tx_is_out,
        'direction',   case when e.tx_is_in then 'incoming' when e.tx_is_out then 'outgoing' else 'unknown' end,
        'days_between_tx_and_doc',
          case
            when e.transaction_date_unix is not null and e.document_date_unix is not null
              then (abs(e.transaction_date_unix - e.document_date_unix) / 86400)::int
            else null
          end
      )::text,
      nullif(nt.note_text,'')
    from notes_text nt
    join enrich e using (transaction_id, document_id)
    returning reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores as (
    insert into public.reconciliations_match_scores (
      reconciliation_id, company_id, confidence, numerator, total_weight,
      date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
      date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
      date_score,  invoice_score,  order_score,  merchant_score,  amount_score,  currency_score,  customer_score
    )
    select
      r.reconciliation_id,
      d.document_company_id as company_id,
      r.confidence,
      d.total_numerator,
      d.dynamic_total_weight,
      d.date_match,
      d.invoice_number_match,
      d.order_number_match,
      d.merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      d.order_weight,
      d.merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      d.order_score,
      d.merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    from inserted_reconciliations r
    join scored_with_confidence d
      on d.transaction_id = r.transaction_id
     and d.document_id    = r.document_id
    returning 1
  ),

  final_counts as (
    select
      (select count(*) from top_per_transaction)      as top_per_transaction_count,
      (select count(*) from top_per_document)         as top_per_document_count,
      (select count(*) from final_candidates)         as passed_confidence_count,
      (select count(*) from inserted_reconciliations) as inserted_count,
      (select count(*) from base_candidates)          as candidates_count,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_total)    as seed_counts_total_json,
      (select coalesce(jsonb_object_agg(seed, ct), '{}'::jsonb) from seed_counts_distinct) as seed_counts_distinct_json,
      (select count(*) from transactions_scope)       as total_tx_count,
      (select count(*) from document_scope)           as total_doc_count
  )
  select
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count,
    seed_counts_total_json,
    seed_counts_distinct_json,
    total_tx_count,
    total_doc_count
  into
    v_summary_top_per_tx,
    v_summary_top_per_doc,
    v_summary_passed_count,
    v_inserted_count,
    v_candidates_count,
    v_seed_counts_total,
    v_seed_counts_distinct,
    v_total_tx,
    v_total_docs
  from final_counts;

  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_end', 'function', 'ok', null,
    p_company_id, null, null, null,
    v_inserted_count, null, null, v_elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'transactions_scanned', v_total_tx,
      'documents_considered',  v_total_docs,
      'top_per_tx', v_summary_top_per_tx,
      'top_per_doc', v_summary_top_per_doc,
      'passed_confidence', v_summary_passed_count,
      'seed_candidates_by_logic',  v_seed_counts_total,
      'seed_unique_pairs_by_logic', v_seed_counts_distinct
    )
  );

  return jsonb_build_object(
    'msg',               v_status_message,
    'time_ms',           v_elapsed_ms,
    'transactions_scanned', v_total_tx,
    'documents_considered',  v_total_docs,
    'top_per_tx',        v_summary_top_per_tx,
    'top_per_doc',       v_summary_top_per_doc,
    'passed_confidence', v_summary_passed_count,
    'seed_candidates_by_logic',  v_seed_counts_total,
    'seed_unique_pairs_by_logic', v_seed_counts_distinct
  );

exception when others then
  v_elapsed_ms := floor(extract(epoch from (clock_timestamp() - v_start_time)) * 1000)::bigint;
  get stacked diagnostics
    v_err_state   = returned_sqlstate,
    v_err_msg     = message_text,
    v_err_detail  = pg_exception_detail,
    v_err_hint    = pg_exception_hint,
    v_err_context = pg_exception_context;

  insert into public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  values (
    v_log_run_id, v_engine_version, 'run_error', 'function', 'error', v_err_msg,
    p_company_id, null, null, null,
    null, null, null, v_elapsed_ms,
    jsonb_build_object(
      'sqlstate', v_err_state,
      'detail',   v_err_detail,
      'hint',     v_err_hint,
      'context',  v_err_context
    )
  );

  return jsonb_build_object(
    'msg','error','time_ms',v_elapsed_ms,
    'transactions_scanned',0,'documents_considered',0,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',v_err_msg,'sqlstate',v_err_state
  );
end;
$$;

alter function athena_reconcile_general_v1_9_p(unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v1_9_p(unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v1_9_p(unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v1_9_p(unknown, unknown) to service_role;

create or replace function gen_descriptor_id(descriptor text DEFAULT 'sws'::text) returns text
    language plpgsql
as
$$
BEGIN
  RETURN descriptor || '_' || encode(substring(digest(gen_random_uuid()::text, 'sha256') for 6), 'hex');
END;
$$;

alter function gen_descriptor_id(unknown) owner to postgres;

grant execute on function gen_descriptor_id(unknown) to anon;

grant execute on function gen_descriptor_id(unknown) to authenticated;

grant execute on function gen_descriptor_id(unknown) to service_role;

-- Cyclic dependencies found

create or replace function reconciliation_weights_auto_v4(p_company_id uuid DEFAULT NULL::uuid, p_days integer DEFAULT 365, p_min_rows integer DEFAULT 50, p_conf_min numeric DEFAULT 80, p_half_life_days integer DEFAULT 365, p_verified_only boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_rows_upserted int := 0;
  v_companies     int := 0;
BEGIN
  WITH
  base AS (
    SELECT
      r.company_id,
      r.confidence,
      r.verified,
      s.date_match::int      AS hit_date,
      s.invoice_match::int   AS hit_inv,
      s.order_match::int     AS hit_ord,
      s.merchant_match::int  AS hit_mer,
      (s.amount_match)::int  AS hit_amt,
      s.currency_match::int  AS hit_cur,
      s.customer_match::int  AS hit_cus,
      s.created_at
    FROM public.reconciliations_match_scores s
    JOIN public.reconciliations r ON r.reconciliation_id = s.reconciliation_id
    WHERE s.created_at >= now() - make_interval(days => p_days)
      AND (p_company_id IS NULL OR r.company_id = p_company_id)
      AND (NOT p_verified_only OR r.verified IS TRUE)
  ),

  -- recency * confidence weight (with tiny floor)
  weighted AS (
    SELECT
      company_id,
      GREATEST(
        0.05,
        EXP(-LN(2) * (EXTRACT(EPOCH FROM (now() - created_at))/86400.0)
            / NULLIF(p_half_life_days,0))
        * CASE
            WHEN p_conf_min IS NULL OR p_conf_min = 0
              THEN LEAST(1.0, GREATEST(0.0, COALESCE(confidence, 50)::numeric / 100.0))
            ELSE LEAST(1.0, GREATEST(0.0, (COALESCE(confidence, p_conf_min)::numeric - p_conf_min) / 20.0))
          END
      ) AS w,
      hit_date, hit_inv, hit_ord, hit_mer, hit_amt, hit_cur, hit_cus
    FROM base
  ),

  agg AS (
    SELECT
      company_id,
      SUM(w)            AS n_w,
      SUM(w*hit_date)   AS x_date,
      SUM(w*hit_inv)    AS x_inv,
      SUM(w*hit_ord)    AS x_ord,
      SUM(w*hit_mer)    AS x_mer,
      SUM(w*hit_amt)    AS x_amt,
      SUM(w*hit_cur)    AS x_cur,
      SUM(w*hit_cus)    AS x_cus
    FROM weighted
    GROUP BY company_id
  ),

  eligible AS (
    SELECT * FROM agg WHERE n_w >= p_min_rows
  ),

  wilson AS (
    SELECT company_id, n_w, 1.96::numeric AS z,
           x_date, x_inv, x_ord, x_mer, x_amt, x_cur, x_cus
    FROM eligible
  ),

  rates AS (
    SELECT
      company_id, n_w,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_date/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_date/n_w)*(1 - (x_date/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_date,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_inv/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_inv/n_w)*(1 - (x_inv/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_inv,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_ord/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_ord/n_w)*(1 - (x_ord/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_ord,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_mer/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_mer/n_w)*(1 - (x_mer/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_mer,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_amt/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_amt/n_w)*(1 - (x_amt/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_amt,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_cur/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_cur/n_w)*(1 - (x_cur/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_cur,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_cus/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_cus/n_w)*(1 - (x_cus/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_cus
    FROM wilson
  ),

  -- Map LBs into anchored bands (importance-first)
  desired AS (
    SELECT company_id, 'date'            AS weight_key, ROUND(0.80 + 0.40 * lb_date, 2) AS final_value FROM rates
    UNION ALL
    SELECT company_id, 'invoice_number',  ROUND(1.60 + 0.20 * lb_inv,  2) FROM rates
    UNION ALL
    SELECT company_id, 'order_number',    ROUND(1.00 + 0.50 * lb_ord,  2) FROM rates
    UNION ALL
    SELECT company_id, 'merchant_id',     ROUND(1.00 + 0.40 * lb_mer,  2) FROM rates
    UNION ALL
    SELECT company_id, 'amount_total',    ROUND(1.20 + 0.50 * lb_amt,  2) FROM rates
    UNION ALL
    SELECT company_id, 'currency',        ROUND(0.05 + 0.15 * lb_cur,  2) FROM rates
    UNION ALL
    SELECT company_id, 'customer_name',   ROUND(0.40 + 0.40 * lb_cus,  2) FROM rates
  ),

  upserted AS (
    INSERT INTO public.reconciliations_weight_registry
      (company_id, merchant_id, weight_key, weight_value, active, created_at)
    SELECT
      d.company_id, NULL, d.weight_key,
      LEAST(2.00, GREATEST(0.01, d.final_value)), TRUE, now()
    FROM desired d
    ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
    DO UPDATE SET
      weight_value = EXCLUDED.weight_value,
      active       = TRUE,
      created_at   = now()
    RETURNING 1
  )

  SELECT COUNT(*) INTO v_rows_upserted FROM upserted;

  SELECT COUNT(*) INTO v_companies FROM eligible;

  RETURN jsonb_build_object(
    'status',               'ok',
    'companies_considered', v_companies,
    'rows_upserted',        COALESCE(v_rows_upserted,0),
    'lookback_days',        p_days,
    'min_effective_rows',   p_min_rows,
    'conf_min',             p_conf_min,
    'half_life_days',       p_half_life_days,
    'verified_only',        p_verified_only,
    'filtered_company',     p_company_id::text
  );
END;
$$;

alter function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

create or replace function reconciliation_weights_auto_v4(p_company_id uuid DEFAULT NULL::uuid, p_days integer DEFAULT 90, p_min_rows numeric DEFAULT 200, p_conf_min numeric DEFAULT 80, p_half_life_days numeric DEFAULT 45, p_verified_only boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_rows_upserted int := 0;
  v_companies     int := 0;
BEGIN
  WITH
  base AS (
    SELECT
      r.company_id,
      r.confidence,
      r.verified,
      s.date_match::int      AS hit_date,
      s.invoice_match::int   AS hit_inv,
      s.order_match::int     AS hit_ord,
      s.merchant_match::int  AS hit_mer,
      (s.amount_match)::int  AS hit_amt,
      s.currency_match::int  AS hit_cur,
      s.customer_match::int  AS hit_cus,
      s.created_at
    FROM public.reconciliations_match_scores s
    JOIN public.reconciliations r ON r.reconciliation_id = s.reconciliation_id
    WHERE s.created_at >= now() - make_interval(days => p_days)
      AND (p_company_id IS NULL OR r.company_id = p_company_id)
      AND (NOT p_verified_only OR r.verified IS TRUE)
  ),

  -- recency * confidence weight (with tiny floor)
  weighted AS (
    SELECT
      company_id,
      GREATEST(
        0.05,
        EXP(-LN(2) * (EXTRACT(EPOCH FROM (now() - created_at))/86400.0)
            / NULLIF(p_half_life_days,0))
        * CASE
            WHEN p_conf_min IS NULL OR p_conf_min = 0
              THEN LEAST(1.0, GREATEST(0.0, COALESCE(confidence, 50)::numeric / 100.0))
            ELSE LEAST(1.0, GREATEST(0.0, (COALESCE(confidence, p_conf_min)::numeric - p_conf_min) / 20.0))
          END
      ) AS w,
      hit_date, hit_inv, hit_ord, hit_mer, hit_amt, hit_cur, hit_cus
    FROM base
  ),

  agg AS (
    SELECT
      company_id,
      SUM(w)            AS n_w,
      SUM(w*hit_date)   AS x_date,
      SUM(w*hit_inv)    AS x_inv,
      SUM(w*hit_ord)    AS x_ord,
      SUM(w*hit_mer)    AS x_mer,
      SUM(w*hit_amt)    AS x_amt,
      SUM(w*hit_cur)    AS x_cur,
      SUM(w*hit_cus)    AS x_cus
    FROM weighted
    GROUP BY company_id
  ),

  eligible AS (
    SELECT * FROM agg WHERE n_w >= p_min_rows
  ),

  wilson AS (
    SELECT company_id, n_w, 1.96::numeric AS z,
           x_date, x_inv, x_ord, x_mer, x_amt, x_cur, x_cus
    FROM eligible
  ),

  rates AS (
    SELECT
      company_id, n_w,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_date/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_date/n_w)*(1 - (x_date/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_date,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_inv/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_inv/n_w)*(1 - (x_inv/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_inv,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_ord/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_ord/n_w)*(1 - (x_ord/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_ord,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_mer/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_mer/n_w)*(1 - (x_mer/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_mer,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_amt/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_amt/n_w)*(1 - (x_amt/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_amt,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_cur/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_cur/n_w)*(1 - (x_cur/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_cur,
      GREATEST(0, LEAST(1,
        CASE WHEN n_w <= 0 THEN 0
             ELSE (
               ((x_cus/n_w) + (z*z)/(2*n_w))
               - z * SQRT( ((x_cus/n_w)*(1 - (x_cus/n_w)) + (z*z)/(4*n_w)) / n_w )
             ) / (1 + (z*z)/n_w)
        END)) AS lb_cus
    FROM wilson
  ),

  -- Map LBs into anchored bands (importance-first)
  desired AS (
    SELECT company_id, 'date'            AS weight_key, ROUND(0.80 + 0.40 * lb_date, 2) AS final_value FROM rates
    UNION ALL
    SELECT company_id, 'invoice_number',  ROUND(1.60 + 0.20 * lb_inv,  2) FROM rates
    UNION ALL
    SELECT company_id, 'order_number',    ROUND(1.00 + 0.50 * lb_ord,  2) FROM rates
    UNION ALL
    SELECT company_id, 'merchant_id',     ROUND(1.00 + 0.40 * lb_mer,  2) FROM rates
    UNION ALL
    SELECT company_id, 'amount_total',    ROUND(1.20 + 0.50 * lb_amt,  2) FROM rates
    UNION ALL
    SELECT company_id, 'currency',        ROUND(0.05 + 0.15 * lb_cur,  2) FROM rates
    UNION ALL
    SELECT company_id, 'customer_name',   ROUND(0.40 + 0.40 * lb_cus,  2) FROM rates
  ),

  upserted AS (
    INSERT INTO public.reconciliations_weight_registry
      (company_id, merchant_id, weight_key, weight_value, active, created_at)
    SELECT
      d.company_id, NULL, d.weight_key,
      LEAST(2.00, GREATEST(0.01, d.final_value)), TRUE, now()
    FROM desired d
    ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
    DO UPDATE SET
      weight_value = EXCLUDED.weight_value,
      active       = TRUE,
      created_at   = now()
    RETURNING 1
  )

  SELECT COUNT(*) INTO v_rows_upserted FROM upserted;

  SELECT COUNT(*) INTO v_companies FROM eligible;

  RETURN jsonb_build_object(
    'status',               'ok',
    'companies_considered', v_companies,
    'rows_upserted',        COALESCE(v_rows_upserted,0),
    'lookback_days',        p_days,
    'min_effective_rows',   p_min_rows,
    'conf_min',             p_conf_min,
    'half_life_days',       p_half_life_days,
    'verified_only',        p_verified_only,
    'filtered_company',     p_company_id::text
  );
END;
$$;

alter function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function reconciliation_weights_auto_v4(unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

