\q
--
-- PostgreSQL database dump
--

-- Dumped from database version 15.13
-- Dumped by pg_dump version 15.13

SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;

--
-- Name: common; Type: SCHEMA; Schema: -; Owner: postgres
--

CREATE SCHEMA common;


ALTER SCHEMA common OWNER TO postgres;

--
-- Name: dataimpulsepool; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.dataimpulsepool AS ENUM (
    'datacenter',
    'residential',
    'mobile',
    'residential_premium'
);


ALTER TYPE common.dataimpulsepool OWNER TO postgres;

--
-- Name: datetime_aggregate; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.datetime_aggregate AS ENUM (
    'minute',
    'hour',
    'day'
);


ALTER TYPE common.datetime_aggregate OWNER TO postgres;

--
-- Name: discountype; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.discountype AS ENUM (
    'Persent',
    'Unit'
);


ALTER TYPE common.discountype OWNER TO postgres;

--
-- Name: errors_period; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.errors_period AS ENUM (
    'week',
    'month'
);


ALTER TYPE common.errors_period OWNER TO postgres;

--
-- Name: format_promise; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.format_promise AS (
	text text,
	args text[]
);


ALTER TYPE common.format_promise OWNER TO postgres;

--
-- Name: is_valid_domain(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.is_valid_domain(input text) RETURNS boolean
    LANGUAGE plpgsql IMMUTABLE
    AS $_$
BEGIN
    RETURN input ~* '^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.(?!-)[A-Za-z0-9-]{1,63}(?<!-))*\.?$';
END;
$_$;


ALTER FUNCTION common.is_valid_domain(input text) OWNER TO postgres;

--
-- Name: is_valid_ip(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.is_valid_ip(input text) RETURNS boolean
    LANGUAGE plpgsql IMMUTABLE
    AS $$
BEGIN
    PERFORM input::inet;
    RETURN TRUE;
EXCEPTION WHEN others THEN
    RETURN FALSE;
END;
$$;


ALTER FUNCTION common.is_valid_ip(input text) OWNER TO postgres;

--
-- Name: host; Type: DOMAIN; Schema: common; Owner: postgres
--

CREATE DOMAIN common.host AS text
	CONSTRAINT host_check CHECK ((common.is_valid_ip(VALUE) OR common.is_valid_domain(VALUE)));


ALTER DOMAIN common.host OWNER TO postgres;

--
-- Name: period; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.period AS ENUM (
    'day',
    'week',
    'month',
    '3months',
    '6months'
);


ALTER TYPE common.period OWNER TO postgres;

--
-- Name: is_valid_port(integer); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.is_valid_port(input integer) RETURNS boolean
    LANGUAGE plpgsql IMMUTABLE
    AS $$
BEGIN
    RETURN input >= 1 AND input <= 65535;
END;
$$;


ALTER FUNCTION common.is_valid_port(input integer) OWNER TO postgres;

--
-- Name: port; Type: DOMAIN; Schema: common; Owner: postgres
--

CREATE DOMAIN common.port AS integer
	CONSTRAINT port_check CHECK (common.is_valid_port(VALUE));


ALTER DOMAIN common.port OWNER TO postgres;

--
-- Name: price_calculation; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.price_calculation AS (
	base_price numeric,
	pool_price numeric,
	volume_discount numeric,
	volume_discount_amount numeric,
	promo_discount numeric,
	promo_discount_amount numeric,
	final_price numeric
);


ALTER TYPE common.price_calculation OWNER TO postgres;

--
-- Name: protocols; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.protocols AS ENUM (
    'http',
    'socks5'
);


ALTER TYPE common.protocols OWNER TO postgres;

--
-- Name: provider; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.provider AS ENUM (
    'Dataimpulse'
);


ALTER TYPE common.provider OWNER TO postgres;

--
-- Name: proxyregion; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.proxyregion AS ENUM (
    'aus',
    'jpn',
    'bra',
    'sgp',
    'eur',
    'usa',
    'auto'
);


ALTER TYPE common.proxyregion OWNER TO postgres;

--
-- Name: referralreasontype; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.referralreasontype AS ENUM (
    'AdminAction',
    'ReferalPurchase',
    'OwnPurchase'
);


ALTER TYPE common.referralreasontype OWNER TO postgres;

--
-- Name: role_type; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.role_type AS ENUM (
    'vgate_admin',
    'vgate_user',
    'vgate_moderator',
    'vgate_provider',
    'vgate_support'
);


ALTER TYPE common.role_type OWNER TO postgres;

--
-- Name: schematype; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.schematype AS ENUM (
    'inner',
    'gateway'
);


ALTER TYPE common.schematype OWNER TO postgres;

--
-- Name: sha256; Type: DOMAIN; Schema: common; Owner: postgres
--

CREATE DOMAIN common.sha256 AS character varying(64)
	CONSTRAINT sha256_check CHECK (((VALUE)::text ~ '^[0-9a-fA-F]{64}$'::text));


ALTER DOMAIN common.sha256 OWNER TO postgres;

--
-- Name: user_notification; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.user_notification AS (
	user_id text,
	message common.format_promise
);


ALTER TYPE common.user_notification OWNER TO postgres;

--
-- Name: withdraw_request_status; Type: TYPE; Schema: common; Owner: postgres
--

CREATE TYPE common.withdraw_request_status AS ENUM (
    'pending',
    'approved',
    'rejected',
    'cancelled',
    'completed'
);


ALTER TYPE common.withdraw_request_status OWNER TO postgres;

--
-- Name: add_prevent_created_at_trigger_to_new_immutable_child(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.add_prevent_created_at_trigger_to_new_immutable_child() RETURNS event_trigger
    LANGUAGE plpgsql
    AS $$
DECLARE
    child_table_name TEXT;
BEGIN
    FOR child_table_name IN
        SELECT objid::regclass::text
        FROM pg_event_trigger_ddl_commands()
        WHERE command_tag = 'CREATE TABLE'
    LOOP
        -- Check if the new table inherits from base
        IF EXISTS (
            SELECT 1 FROM pg_inherits
            WHERE inhrelid = child_table_name::regclass
            AND inhparent::regclass::text = 'common.created_at'
        ) THEN
            -- Add trigger to the new table
            EXECUTE format(
                'CREATE TRIGGER prevent_created_at_update_%s BEFORE UPDATE ON %s FOR EACH ROW EXECUTE FUNCTION common.prevent_created_at_update()',
                replace(replace(child_table_name, '"', ''), '.', '_'),
                child_table_name
            );
        END IF;
    END LOOP;
END $$;


ALTER FUNCTION common.add_prevent_created_at_trigger_to_new_immutable_child() OWNER TO postgres;

--
-- Name: add_translation(jsonb); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.add_translation(jsonb) RETURNS void
    LANGUAGE plpgsql
    AS $_$
DECLARE
    hash text;
    language_legacy_code vgate.language;
    content text;
BEGIN
    IF NOT ($1 ? 'hash' AND $1 ? 'language_legacy_code' AND $1 ? 'content') THEN
        RAISE EXCEPTION 'Invalid JSON input: required keys missing';
    END IF;

    hash := $1->>'hash';
    language_legacy_code := $1->>'language_legacy_code';
    content := $1->>'content';

    PERFORM common.add_translation(hash, language_legacy_code, content);
END;
$_$;


ALTER FUNCTION common.add_translation(jsonb) OWNER TO postgres;

--
-- Name: add_translation(text, vgate.language, text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.add_translation(hash text, language_legacy_code vgate.language, content text) RETURNS void
    LANGUAGE plpgsql
    AS $$
DECLARE
    lang_id INTEGER;
    _country_id INTEGER;
BEGIN
    SELECT language_id, country_id FROM common.get_language_id(language_legacy_code) INTO lang_id, _country_id;

    INSERT INTO vgate.translation (text_id, language_id, country_id, content)
    VALUES (hash, lang_id, _country_id, content)
    ON CONFLICT (text_id, language_id, country_id) DO UPDATE SET
        content = EXCLUDED.content,
        updated_at = now();
END;
$$;


ALTER FUNCTION common.add_translation(hash text, language_legacy_code vgate.language, content text) OWNER TO postgres;

--
-- Name: add_trigger_to_new_child(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.add_trigger_to_new_child() RETURNS event_trigger
    LANGUAGE plpgsql
    AS $$
DECLARE
    child_table_name TEXT;
BEGIN
    -- find table that created now
    FOR child_table_name IN
        SELECT objid::regclass::text
        FROM pg_event_trigger_ddl_commands()
        WHERE command_tag = 'CREATE TABLE'
    LOOP
	-- ensure this table inherited from base
        IF EXISTS (
            SELECT 1 FROM pg_inherits
            WHERE inhrelid = child_table_name::regclass
            AND inhparent::regclass::text = 'common.updated_at'
        ) THEN
            -- add triger on new table
            EXECUTE format(
                'CREATE TRIGGER update_updated_at_%s BEFORE UPDATE ON %s FOR EACH ROW EXECUTE FUNCTION common.update_updated_at_column()',
                replace(replace(child_table_name, '"', ''), '.', '_'),
                child_table_name
            );
        END IF;
    END LOOP;
END $$;


ALTER FUNCTION common.add_trigger_to_new_child() OWNER TO postgres;

--
-- Name: admins(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.admins() RETURNS text[]
    LANGUAGE plpgsql
    AS $$
BEGIN
    RETURN ARRAY(SELECT user_id FROM legacy.all_user WHERE role = 'vgate_admin');
END;
$$;


ALTER FUNCTION common.admins() OWNER TO postgres;

--
-- Name: apply_discounts(numeric, integer, text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.apply_discounts(price numeric, amount integer, promocode text DEFAULT NULL::text) RETURNS numeric
    LANGUAGE plpgsql
    AS $$
DECLARE
    promo_info RECORD;
BEGIN
    RAISE LOG 'create invoid promocode: %', promocode;
    IF promocode IS NOT NULL THEN
      SELECT discount, is_global FROM legacy.disc_promocode WHERE name = promocode INTO promo_info;
      IF promo_info IS NULL THEN
          RAISE sqlstate 'PT422' USING message = 'discount promocode does not exists';
      END IF;

      IF promo_info.discount > 0 THEN
          IF NOT (promo_info.is_global = 1 and amount >= 100) THEN
              price := price - price * promo_info.discount;
          END IF;
      END IF;
    END IF;

    -- Bulk discount
    price := price - price * calculate(amount);

    RETURN price;
END $$;


ALTER FUNCTION common.apply_discounts(price numeric, amount integer, promocode text) OWNER TO postgres;

--
-- Name: array_last(text[]); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.array_last(text[]) RETURNS text
    LANGUAGE plpgsql
    AS $_$
BEGIN
  RETURN $1[array_length($1, 1)];
END;
$_$;


ALTER FUNCTION common.array_last(text[]) OWNER TO postgres;

--
-- Name: array_remove_last(anyarray); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.array_remove_last(anyarray) RETURNS anyarray
    LANGUAGE plpgsql
    AS $_$
BEGIN
  RETURN $1[1:array_length($1, 1) - 1];
END;
$_$;


ALTER FUNCTION common.array_remove_last(anyarray) OWNER TO postgres;

--
-- Name: build_log_source(text[]); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.build_log_source(path text[]) RETURNS text
    LANGUAGE sql
    AS $$
    SELECT array_to_string("path", '::');
$$;


ALTER FUNCTION common.build_log_source(path text[]) OWNER TO postgres;

--
-- Name: calculate_detailed_price(integer, common.dataimpulsepool, text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.calculate_detailed_price(amount integer, pool common.dataimpulsepool, promocode text DEFAULT NULL::text) RETURNS common.price_calculation
    LANGUAGE plpgsql
    AS $$
DECLARE
    result common.price_calculation;
    promo_info RECORD;
    pool_exists boolean;
BEGIN
    SELECT EXISTS (
        SELECT 1 FROM legacy.price p WHERE p.pool::common.dataimpulsepool = calculate_detailed_price.pool
    ) INTO pool_exists;

    IF NOT pool_exists THEN
        RAISE sqlstate 'PT404' USING 
            message = 'pool not found',
            detail = format('Pool "%s" does not exist', pool);
    END IF;

    IF amount < 1 THEN
        RAISE sqlstate 'PT400' USING
            message = 'invalid amount',
            detail = 'Amount must be greater than 0';
    END IF;

    SELECT p.gb_cost_usd INTO result.pool_price
    FROM legacy.price p
    WHERE p.pool::common.dataimpulsepool = calculate_detailed_price.pool;

    -- Bulk discount
    result.base_price := result.pool_price * amount;

    result.volume_discount := calculate(amount);
    result.volume_discount_amount := result.base_price * result.volume_discount;
    
    result.final_price := result.base_price - result.volume_discount_amount;

    -- Apply promo discount if exists
    IF promocode IS NOT NULL THEN
        SELECT discount, is_global 
        FROM legacy.disc_promocode 
        WHERE name = promocode 
        INTO promo_info;

        IF promo_info IS NULL THEN
            RAISE sqlstate 'PT422' USING 
                message = 'discount promocode does not exist';
        END IF;

        IF NOT common.is_available_discount_promocode(promocode, amount) THEN
            RAISE sqlstate 'PT422' USING 
                message = 'promocode expired or not available',
                detail = format('Promocode "%s" cannot be used for this purchase', promocode);
        END IF;

        IF promo_info.discount > 0 THEN
            IF promo_info.is_global = 1 AND amount >= 100 THEN
                RAISE sqlstate 'PT422' USING
                    message = 'global promocode not available for bulk purchase',
                    detail = 'Global promocodes cannot be used for purchases of 100GB or more';
            END IF;

            result.promo_discount := promo_info.discount;
            result.promo_discount_amount := result.final_price * result.promo_discount;
            result.final_price := result.final_price - result.promo_discount_amount;
        END IF;
    END IF;

    IF result.final_price < 0 THEN
        RAISE sqlstate 'PT500' USING
            message = 'invalid price calculation',
            detail = 'Final price calculation resulted in negative value';
    END IF;

    RETURN result;
END;
$$;


ALTER FUNCTION common.calculate_detailed_price(amount integer, pool common.dataimpulsepool, promocode text) OWNER TO postgres;

--
-- Name: FUNCTION calculate_detailed_price(amount integer, pool common.dataimpulsepool, promocode text); Type: COMMENT; Schema: common; Owner: postgres
--

COMMENT ON FUNCTION common.calculate_detailed_price(amount integer, pool common.dataimpulsepool, promocode text) IS 'Calculates detailed price breakdown including base price, all discounts and final price. Includes validation for pool existence, amount, promocode validity, and ensures non-negative final price.';


--
-- Name: cannot_create_table_that_not_inherits_from_created_at(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.cannot_create_table_that_not_inherits_from_created_at() RETURNS event_trigger
    LANGUAGE plpgsql
    AS $$
DECLARE
    child_table_name TEXT;
    info RECORD;
BEGIN
    FOR child_table_name IN
        SELECT objid::regclass::text
        FROM pg_event_trigger_ddl_commands()
        WHERE command_tag = 'CREATE TABLE'
    LOOP
        IF NOT EXISTS (
            SELECT 1 FROM pg_inherits
            WHERE inhrelid = child_table_name::regclass
            AND (inhparent::regclass::text = 'common.created_at'
                OR inhparent::regclass::text = 'created_at')
        ) THEN
	    SELECT inhparent FROM pg_inherits WHERE inhrelid = child_table_name::regclass INTO info;
	    RAISE DEBUG '%', info.inhparent::regclass::text;
            RAISE DEBUG '%', child_table_name;
            RAISE EXCEPTION 'Cannot create table that not inherited from "common.created_at" table';
        END IF;
    END LOOP;
END $$;


ALTER FUNCTION common.cannot_create_table_that_not_inherits_from_created_at() OWNER TO postgres;

--
-- Name: confirm_email_html(text, text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.confirm_email_html(username text, confirmation_link text) RETURNS text
    LANGUAGE plpgsql
    AS $_$
BEGIN
  RETURN format($html$
    <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #ffffff; border-radius: 8px; border: 1px solid #e0e0e0;">
      <div style="text-align: center; margin-bottom: 30px;">
        <h1 style="color: #1a1a1a; font-size: 24px; margin-bottom: 10px;">Confirm Your Email Address</h1>
      </div>
    
      <div style="color: #4a4a4a; font-size: 16px; line-height: 1.6; margin-bottom: 30px;">
        <p>Hello %s</p>
        <p>Thank you for registering with VProxy. To complete your registration and verify your email address, please click the button below:</p>
      </div>
    
      <div style="text-align: center; margin-bottom: 30px;">
        <a href="%s" style="background-color: #f59e0b; color: #ffffff; padding: 12px 24px; border-radius: 4px; text-decoration: none; font-weight: bold; display: inline-block;">
          Confirm Email Address
        </a>
      </div>
    
      <div style="color: #666666; font-size: 14px; border-top: 1px solid #e0e0e0; padding-top: 20px;">
        <p>If you didn't create an account with VProxy, please ignore this email.</p>
        <p>If you're having trouble clicking the button, copy and paste this link into your browser:</p>
        <p style="word-break: break-all; color: #0066cc;">%s</p>
      </div>
    </div>
  $html$, username, confirmation_link, confirmation_link);
END
$_$;


ALTER FUNCTION common.confirm_email_html(username text, confirmation_link text) OWNER TO postgres;

--
-- Name: current_role(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common."current_role"() RETURNS common.role_type
    LANGUAGE plpgsql
    AS $$
BEGIN
    RETURN current_setting('request.jwt.claims')::JSONB->>'role';
END $$;


ALTER FUNCTION common."current_role"() OWNER TO postgres;

--
-- Name: current_user_id(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.current_user_id() RETURNS text
    LANGUAGE plpgsql
    AS $$
DECLARE
    user_id TEXT;
BEGIN
    -- Get user ID from JWT for normal users
    user_id := current_setting('request.jwt.claims')::JSONB->>'sub';
    
    -- If user is a provider (bot/telegram), get user ID from X-Tg-Id header
    IF common.is_provider() THEN
        user_id := common.get_header('X-Tg-Id');
        IF user_id IS NULL THEN
            RAISE EXCEPTION 'X-Tg-Id header is required for provider requests';
        END IF;
    END IF;
    
    RETURN user_id;
END $$;


ALTER FUNCTION common.current_user_id() OWNER TO postgres;

--
-- Name: current_user_id_with_ok(boolean); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.current_user_id_with_ok(missing_ok boolean DEFAULT false) RETURNS text
    LANGUAGE plpgsql
    AS $$
DECLARE
    user_id TEXT;
BEGIN
    -- Get user ID from JWT for normal users
    user_id := current_setting('request.jwt.claims')::JSONB->>'sub';
    
    -- If user is a provider (bot/telegram), get user ID from X-Tg-Id header
    IF common.is_provider() THEN
        user_id := common.get_header('X-Tg-Id');
        IF user_id IS NULL THEN
            RAISE EXCEPTION 'X-Tg-Id header is required for provider requests';
        END IF;
    END IF;

    IF NOT missing_ok AND (user_id IS NULL OR trim(user_id) = '') THEN
        RAISE sqlstate 'PT401' USING message = 'User authentication required';
    END IF;
    
    RETURN user_id;
END $$;


ALTER FUNCTION common.current_user_id_with_ok(missing_ok boolean) OWNER TO postgres;

--
-- Name: fetch_currency(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.fetch_currency() RETURNS void
    LANGUAGE plpgsql
    AS $$
DECLARE
  response JSONB;
  price_id INT;
  currency_id INT;
BEGIN
  RAISE LOG 'fetch_currency cron';
  SELECT "content" FROM public.http_get('https://min-api.cryptocompare.com/data/price?fsym=RUB&tsyms=USD,EUR,UAH,KZT') INTO response;

  SELECT "currency_price".id FROM "currency_price" 
  INTO price_id
  JOIN "currency" ON "currency"."id" = "currency_price"."from"
  WHERE "currency"."code" = 'RUB';

  IF price_id IS NOT NULL THEN
    UPDATE "currency_price" SET "to" = response WHERE "from" = price_id;
  ELSE
    SELECT "currency".id FROM "currency" 
    INTO currency_id
    WHERE "code" = 'RUB'; 
    INSERT INTO "currency_price" ("from", "to") VALUES (currency_id, response);
  END IF;
END;
$$;


ALTER FUNCTION common.fetch_currency() OWNER TO postgres;

--
-- Name: format_traffic(bigint); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.format_traffic(bytes bigint) RETURNS text
    LANGUAGE plpgsql
    AS $$
BEGIN
    IF bytes >= 1073741824 THEN
        RETURN ROUND(bytes::NUMERIC / 1073741824, 2) || ' GB';
    ELSIF bytes >= 1048576 THEN
        RETURN ROUND(bytes::NUMERIC / 1048576, 2) || ' MB';
    ELSIF bytes >= 1024 THEN
        RETURN ROUND(bytes::NUMERIC / 1024, 2) || ' KB';
    ELSE
        RETURN bytes || ' B';
    END IF;
END;
$$;


ALTER FUNCTION common.format_traffic(bytes bigint) OWNER TO postgres;

--
-- Name: gateway_admin_role(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.gateway_admin_role(gateway_schema text) RETURNS text
    LANGUAGE plpgsql
    AS $$
BEGIN
    RETURN gateway_schema || '_admin';
END $$;


ALTER FUNCTION common.gateway_admin_role(gateway_schema text) OWNER TO postgres;

--
-- Name: gateway_provider_role(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.gateway_provider_role(gateway_schema text DEFAULT NULL::text) RETURNS text
    LANGUAGE plpgsql
    AS $$
BEGIN
    -- RETURN gateway_schema || '_provider';
    RETURN 'vgate_provider';
END $$;


ALTER FUNCTION common.gateway_provider_role(gateway_schema text) OWNER TO postgres;

--
-- Name: gateway_user_role(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.gateway_user_role(gateway_schema text DEFAULT NULL::text) RETURNS text
    LANGUAGE plpgsql
    AS $$
BEGIN
    -- RETURN gateway_schema || '_user';
    RETURN 'vgate_user';
END $$;


ALTER FUNCTION common.gateway_user_role(gateway_schema text) OWNER TO postgres;

--
-- Name: generate_promocode_name(integer); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.generate_promocode_name(length integer DEFAULT 14) RETURNS text
    LANGUAGE plpgsql
    AS $$
DECLARE
  chars TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  result TEXT := '';
  i INT;
BEGIN
  FOR i IN 1..length LOOP
    result := result || substr(chars, floor(random() * length(chars) + 1)::int, 1);
  END LOOP;
  RETURN result;
END;
$$;


ALTER FUNCTION common.generate_promocode_name(length integer) OWNER TO postgres;

--
-- Name: generate_scram_hash(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.generate_scram_hash(password text) RETURNS text
    LANGUAGE plpgsql
    AS $_$
DECLARE
  _salt bytea;
  _iterations integer := 4096; -- Standard iteration count for SCRAM-SHA-256
  _digest_length integer := 32; -- SHA-256 hash length
  _digest_key bytea;
  _stored_key bytea;
  _server_key bytea;
BEGIN
  -- Generate a salt
  SELECT gen_random_bytes(16) INTO _salt;

  -- Derive PBKDF2 digest key from password and salt
  SELECT basic_auth.pbkdf2(_salt, password, _iterations, _digest_length, 'sha256') INTO _digest_key;

  -- Compute stored key and server key
  SELECT
    digest(hmac('Client Key', _digest_key, 'sha256'), 'sha256') AS _stored_key,
    hmac('Server Key', _digest_key, 'sha256') AS _server_key
  INTO _stored_key, _server_key;

  -- Construct SCRAM-SHA-256 hash
  RETURN format(
    'SCRAM-SHA-256$%s:%s$%s:%s',
    _iterations,
    encode(_salt, 'base64'),
    encode(_stored_key, 'base64'),
    encode(_server_key, 'base64')
  );
END; $_$;


ALTER FUNCTION common.generate_scram_hash(password text) OWNER TO postgres;

--
-- Name: get_current_token(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.get_current_token() RETURNS text
    LANGUAGE plpgsql
    AS $$
DECLARE
  token TEXT;
BEGIN
    SELECT dataimpulse_jwt FROM vinn.storage LIMIT 1 INTO token;
    RETURN token;
END; $$;


ALTER FUNCTION common.get_current_token() OWNER TO postgres;

--
-- Name: get_gateway_schema(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.get_gateway_schema() RETURNS text
    LANGUAGE plpgsql
    AS $$
DECLARE
    _gateway_schema TEXT;
    _success BOOLEAN;
BEGIN
    SELECT current_schemas[1] FROM CURRENT_SCHEMAS(false) INTO _gateway_schema;

    EXECUTE format('SELECT %I.schema_is(%L)', _gateway_schema, 'gateway') INTO _success;

    IF NOT _success THEN
	RAISE EXCEPTION 'first schema in SEARCH_PATH is not a `gateway` schema';
    END IF;

    RETURN _inner_schema;
END; $$;


ALTER FUNCTION common.get_gateway_schema() OWNER TO postgres;

--
-- Name: get_header(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.get_header(item text) RETURNS text
    LANGUAGE sql STABLE
    AS $$
    SELECT (current_setting('request.headers', true)::JSON)->>LOWER(item)
$$;


ALTER FUNCTION common.get_header(item text) OWNER TO postgres;

--
-- Name: get_headers(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.get_headers() RETURNS jsonb
    LANGUAGE sql STABLE
    AS $$
    SELECT current_setting('request.headers', true)::JSON
$$;


ALTER FUNCTION common.get_headers() OWNER TO postgres;

--
-- Name: get_inner_schema(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.get_inner_schema() RETURNS text
    LANGUAGE plpgsql
    AS $$
DECLARE
    _inner_schema TEXT;
    _success BOOLEAN;
BEGIN
    SELECT current_schemas[2] FROM CURRENT_SCHEMAS(false) INTO _inner_schema;

    EXECUTE format('SELECT %I.schema_is(%L)', _inner_schema, 'inner') INTO _success;

    IF NOT _success THEN
	RAISE EXCEPTION 'second schema in SEARCH_PATH is not a `inner` schema';
    END IF;

    RETURN _inner_schema;
END; $$;


ALTER FUNCTION common.get_inner_schema() OWNER TO postgres;

--
-- Name: get_jwt_secret(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.get_jwt_secret() RETURNS text
    LANGUAGE plpgsql
    AS $$
BEGIN
    RETURN common.get_setting('jwt_secret');
END;
$$;


ALTER FUNCTION common.get_jwt_secret() OWNER TO postgres;

--
-- Name: get_language_id(vgate.language); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.get_language_id(lang_code vgate.language) RETURNS TABLE(language_id integer, country_id integer)
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
BEGIN
    RETURN QUERY
    SELECT
        l.id,
        c.id
    FROM
        common.language l
    JOIN
        common.country c ON (
            (lang_code = 'rus' AND l.code = 'ru' AND c.code = 'RU') OR
            (lang_code = 'eng' AND l.code = 'en' AND c.code = 'US') OR
            (lang_code NOT IN ('rus', 'eng') AND l.code = 'ru' AND c.code = 'RU')
        )
    LIMIT 1;
END;
$$;


ALTER FUNCTION common.get_language_id(lang_code vgate.language) OWNER TO postgres;

--
-- Name: get_setting(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.get_setting(k text) RETURNS text
    LANGUAGE plpgsql
    AS $$
DECLARE
    result TEXT;
BEGIN
    SELECT
      "value"
    FROM
      "legacy"."system"
    WHERE
      "key" = "k"
  INTO 
    "result";

  IF NOT FOUND THEN
    RAISE EXCEPTION 'System setting % not found', k;
  END IF;

  RETURN result;
END
$$;


ALTER FUNCTION common.get_setting(k text) OWNER TO postgres;

--
-- Name: header_required(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.header_required(name text) RETURNS void
    LANGUAGE plpgsql
    AS $$
BEGIN 
    IF get_header(name) IS NULL THEN     
        RAISE sqlstate 'PT400' USING
            message = format('Required header "%s" is missing', LOWER(name));
    END IF; 
END 
$$;


ALTER FUNCTION common.header_required(name text) OWNER TO postgres;

--
-- Name: FUNCTION header_required(name text); Type: COMMENT; Schema: common; Owner: postgres
--

COMMENT ON FUNCTION common.header_required(name text) IS 'Ensures that a specific HTTP header is present in the request. Raises SQLSTATE PT400 if the header is missing.';


--
-- Name: indoc(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.indoc(text) RETURNS text
    LANGUAGE plpgsql
    AS $_$ 
DECLARE
    lines text[];
    min_indent int := NULL;
    l text;
    result text := '';
    current_indent int;
    start_idx int := 1;
    end_idx int;
BEGIN
    -- Split into lines
    lines := regexp_split_to_array($1, E'\n');
    end_idx := array_length(lines, 1);

    -- Trim leading blank lines
    WHILE start_idx <= end_idx AND lines[start_idx] !~ '\S' LOOP
        start_idx := start_idx + 1;
    END LOOP;

    -- Trim trailing blank lines
    WHILE end_idx >= start_idx AND lines[end_idx] !~ '\S' LOOP
        end_idx := end_idx - 1;
    END LOOP;

    -- Determine min indentation from non-blank lines
    FOR i IN start_idx..end_idx LOOP
        l := lines[i];
        IF l ~ '\S' THEN
            current_indent := length(l) - length(ltrim(l, ' '));
            IF min_indent IS NULL OR current_indent < min_indent THEN
                min_indent := current_indent;
            END IF;
        END IF;
    END LOOP;

    -- Build result
    FOR i IN start_idx..end_idx LOOP
        l := regexp_replace(lines[i], '^[ ]{0,' || min_indent || '}', '');
        result := result || l;
        IF i < end_idx THEN
            result := result || E'\n';
        END IF;
    END LOOP;

    RETURN result;
END;
$_$;


ALTER FUNCTION common.indoc(text) OWNER TO postgres;

--
-- Name: ip_is_local_or_private(inet); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.ip_is_local_or_private(ip inet) RETURNS boolean
    LANGUAGE plpgsql
    AS $$
BEGIN
    IF ip <<= '127.0.0.0/8'::inet OR 
       ip <<= '10.0.0.0/8'::inet OR 
       ip <<= '172.16.0.0/12'::inet OR 
       ip <<= '192.168.0.0/16'::inet OR 
       ip <<= '169.254.0.0/16'::inet OR
       ip <<= '::1/128'::inet OR
       ip <<= 'fc00::/7'::inet OR
       ip <<= 'fe80::/10'::inet THEN
        RETURN true;
    END IF;
    RETURN false;
END;
$$;


ALTER FUNCTION common.ip_is_local_or_private(ip inet) OWNER TO postgres;

--
-- Name: is_available_discount_promocode(text, integer, text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.is_available_discount_promocode(code text, amount integer, user_id text DEFAULT common.current_user_id()) RETURNS boolean
    LANGUAGE plpgsql
    AS $$
DECLARE
  promo_info RECORD;
  user_info RECORD;
  can_use BOOLEAN := true;
BEGIN 
  IF common.is_provider() THEN
    user_id = common.get_header('X-Tg-Id');
    IF user_id IS NULL THEN
      RAISE EXCEPTION 'X-Tg-Id header is required';
    END IF;
  END IF;

  SELECT 
    discount,
    activations,
    is_global,
    first_use,
    name,
    string_to_array(COALESCE(user_for, ''), ',') as user_for
  FROM legacy.disc_promocode
  WHERE name = code
  INTO promo_info;

  IF promo_info IS NULL THEN
    -- RAISE sqlstate 'PT422' USING message = 'promocode does not exist';
    RETURN false;
  END IF;
  
  IF promo_info.activations < 1 THEN
      -- RAISE sqlstate 'PT422' USING message = 'promocode expired'
     RETURN false;
  END IF;

  SELECT 
    buy_num::INT,
    string_to_array(COALESCE("user".used_promo_list, ''), ',') as used_promo
  FROM legacy.user
  WHERE "user".user_id = is_available_discount_promocode.user_id
  LIMIT 1
  INTO user_info;

  -- Check if promo can be used
  IF promo_info.is_global = 0 THEN
    IF promo_info.first_use AND user_info.buy_num > 0 THEN
      RETURN false;
    END IF;

    IF promo_info.name = ANY (user_info.used_promo) THEN
      RETURN false;
    END IF;

    IF user_id != ALL (promo_info.user_for) THEN
      RETURN false;
    END IF;
  ELSE 
   IF amount >= 100 THEN
     RETURN false;
   END IF;
  END IF;

  RETURN true;
END $$;


ALTER FUNCTION common.is_available_discount_promocode(code text, amount integer, user_id text) OWNER TO postgres;

--
-- Name: is_numeric(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.is_numeric(str text) RETURNS boolean
    LANGUAGE plpgsql
    AS $_$
BEGIN
    RETURN str ~ '^-?[0-9]+$';
END
$_$;


ALTER FUNCTION common.is_numeric(str text) OWNER TO postgres;

--
-- Name: is_provider(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.is_provider() RETURNS boolean
    LANGUAGE plpgsql
    AS $$
BEGIN
    -- RETURN CURRENT_USER = 'vgate_provider';
    RETURN current_setting('request.jwt.claims')::JSONB->>'role' = 'vgate_provider';
END $$;


ALTER FUNCTION common.is_provider() OWNER TO postgres;

--
-- Name: merge_users(text, text, boolean); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.merge_users(source_user_id text, target_user_id text, dry_run boolean DEFAULT false) RETURNS TABLE(operation text, table_name text, rows_affected bigint, details text)
    LANGUAGE plpgsql
    AS $$
DECLARE
    _source_exists BOOLEAN;
    _target_exists BOOLEAN;
    _rows_affected BIGINT;
    _operation_count INTEGER := 0;
    _source_user_email TEXT;
    _source_user_password TEXT;
BEGIN
    -- Validation: Check if both users exist
    SELECT EXISTS(SELECT 1 FROM legacy.all_user WHERE user_id = source_user_id) INTO _source_exists;
    SELECT EXISTS(SELECT 1 FROM legacy.all_user WHERE user_id = target_user_id) INTO _target_exists;
    
    IF NOT _source_exists THEN
        RAISE EXCEPTION 'Source user % does not exist', source_user_id;
    END IF;
    
    IF NOT _target_exists THEN
        RAISE EXCEPTION 'Target user % does not exist', target_user_id;
    END IF;
    
    IF source_user_id = target_user_id THEN
        RAISE EXCEPTION 'Source and target user IDs cannot be the same';
    END IF;
    
    RETURN QUERY SELECT 'VALIDATION'::TEXT, 'users_exist'::TEXT, 0::BIGINT, 
        format('Source: %s exists, Target: %s exists', source_user_id, target_user_id);
    
    -- If dry run, show what would be affected
    IF dry_run THEN
        RETURN QUERY SELECT 'DRY_RUN'::TEXT, 'legacy.payment'::TEXT, 
            (SELECT COUNT(*)::BIGINT FROM legacy.payment WHERE user_id = source_user_id),
            'Payments to be transferred';
        
        RETURN QUERY SELECT 'DRY_RUN'::TEXT, 'legacy.invoice'::TEXT,
            (SELECT COUNT(*)::BIGINT FROM legacy.invoice WHERE user_id = source_user_id),
            'Invoices to be transferred';
        
        RETURN QUERY SELECT 'DRY_RUN'::TEXT, 'legacy.request'::TEXT,
            (SELECT COUNT(*)::BIGINT FROM legacy.request WHERE user_id = source_user_id),
            'Withdrawal requests to be transferred';
        
        RETURN QUERY SELECT 'DRY_RUN'::TEXT, 'legacy.admin_ref_users'::TEXT,
            (SELECT COUNT(*)::BIGINT FROM legacy.admin_ref_users WHERE user_id = source_user_id),
            'Admin referral records to be transferred';
        
        RETURN QUERY SELECT 'DRY_RUN'::TEXT, 'legacy.promo_activations'::TEXT,
            (SELECT COUNT(*)::BIGINT FROM legacy.promo_activations WHERE user_id = source_user_id),
            'Promo activations to be transferred';
        
        -- Show legacy.user records and potential balance transfers
        RETURN QUERY 
        WITH user_pools AS (
            SELECT 
                lu_source.pool,
                lu_source.sub_id as source_sub_id,
                lu_target.sub_id as target_sub_id,
                CASE 
                    WHEN lu_target.sub_id IS NOT NULL THEN 'POOL_CONFLICT_MERGE'
                    ELSE 'POOL_TRANSFER'
                END as operation_type
            FROM legacy."user" lu_source
            LEFT JOIN legacy."user" lu_target ON (
                lu_target.user_id = target_user_id 
                AND lu_target.pool = lu_source.pool
            )
            WHERE lu_source.user_id = source_user_id
        )
        SELECT 'DRY_RUN'::TEXT, 'legacy.user'::TEXT,
            COUNT(*)::BIGINT,
            format('Pool operations: %s simple transfers, %s pool conflicts requiring balance merge',
                   COUNT(*) FILTER (WHERE operation_type = 'POOL_TRANSFER'),
                   COUNT(*) FILTER (WHERE operation_type = 'POOL_CONFLICT_MERGE')
            )
        FROM user_pools;
        
        -- Check vgate schema tables if they exist
        IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'vgate' AND table_name = 'bundle') THEN
            RETURN QUERY SELECT 'DRY_RUN'::TEXT, 'vgate.bundle'::TEXT,
                (SELECT COUNT(*)::BIGINT FROM vgate.bundle WHERE user_id::TEXT = source_user_id),
                'Bundles to be transferred';
        END IF;
        
        IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'vgate' AND table_name = 'tg_user') THEN
            RETURN QUERY SELECT 'DRY_RUN'::TEXT, 'vgate.tg_user'::TEXT,
                (SELECT COUNT(*)::BIGINT FROM vgate.tg_user WHERE user_id::TEXT = source_user_id),
                'Telegram users to be transferred';
        END IF;
        
        RETURN;
    END IF;
    
    -- Start transaction for actual merge
    BEGIN
        -- 1. LEGACY SCHEMA TABLES
        
        -- Merge payments (legacy.payment)
        UPDATE legacy.payment 
        SET user_id = target_user_id 
        WHERE user_id = source_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.payment'::TEXT, _rows_affected, 'Transferred payments';
        
        -- Merge invoices (legacy.invoice)
        UPDATE legacy.invoice 
        SET user_id = target_user_id 
        WHERE user_id = source_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.invoice'::TEXT, _rows_affected, 'Transferred invoices';
        
        -- Merge withdrawal requests (legacy.request)
        UPDATE legacy.request 
        SET user_id = target_user_id 
        WHERE user_id = source_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.request'::TEXT, _rows_affected, 'Transferred withdrawal requests';
        
        -- Merge admin referral users (legacy.admin_ref_users)
        -- Handle potential conflicts by updating or ignoring duplicates
        UPDATE legacy.admin_ref_users 
        SET user_id = target_user_id 
        WHERE user_id = source_user_id
        AND NOT EXISTS (
            SELECT 1 FROM legacy.admin_ref_users 
            WHERE user_id = target_user_id 
            AND admin_ref_value = legacy.admin_ref_users.admin_ref_value
            AND source = legacy.admin_ref_users.source
        );
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.admin_ref_users'::TEXT, _rows_affected, 'Transferred admin referral records (avoiding duplicates)';
        
        -- Delete duplicate admin referral records that couldn't be transferred
        DELETE FROM legacy.admin_ref_users 
        WHERE user_id = source_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        IF _rows_affected > 0 THEN
            _operation_count := _operation_count + 1;
            RETURN QUERY SELECT 'DELETE'::TEXT, 'legacy.admin_ref_users'::TEXT, _rows_affected, 'Removed duplicate admin referral records';
        END IF;
        
        -- Merge promo activations (legacy.promo_activations)
        -- For duplicates, merge usage_count
        UPDATE legacy.promo_activations pa1
        SET usage_count = pa1.usage_count + COALESCE(pa2.usage_count, 0)
        FROM legacy.promo_activations pa2
        WHERE pa1.user_id = target_user_id 
        AND pa2.user_id = source_user_id 
        AND pa1.promo_name = pa2.promo_name;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        IF _rows_affected > 0 THEN
            _operation_count := _operation_count + 1;
            RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.promo_activations'::TEXT, _rows_affected, 'Merged duplicate promo activations';
        END IF;
        
        -- Transfer unique promo activations
        UPDATE legacy.promo_activations 
        SET user_id = target_user_id 
        WHERE user_id = source_user_id
        AND NOT EXISTS (
            SELECT 1 FROM legacy.promo_activations 
            WHERE user_id = target_user_id 
            AND promo_name = legacy.promo_activations.promo_name
        );
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.promo_activations'::TEXT, _rows_affected, 'Transferred unique promo activations';
        
        -- Delete remaining source promo activations
        DELETE FROM legacy.promo_activations 
        WHERE user_id = source_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        IF _rows_affected > 0 THEN
            _operation_count := _operation_count + 1;
            RETURN QUERY SELECT 'DELETE'::TEXT, 'legacy.promo_activations'::TEXT, _rows_affected, 'Removed processed promo activations';
        END IF;
        
        -- Merge cross promo activations (legacy.cross_promo_activations)
        UPDATE legacy.cross_promo_activations 
        SET user_id = target_user_id 
        WHERE user_id = source_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.cross_promo_activations'::TEXT, _rows_affected, 'Transferred cross promo activations';
        
        -- Merge legacy user records (legacy.user) - Handle pool conflicts with DataImpulse balance transfer
        -- Since PK is (user_id, pool), we need to merge pools where both users have the same pool
        DECLARE
            pool_record RECORD;
            source_balance_bytes BIGINT;
            source_balance_gb INTEGER;
            target_sub_id BIGINT;
            source_sub_id BIGINT;
            total_transferred INTEGER := 0;
            pools_merged INTEGER := 0;
            pools_transferred INTEGER := 0;
        BEGIN
            -- Process each source user pool record
            FOR pool_record IN 
                SELECT 
                    lu_source.sub_id as source_sub_id,
                    lu_source.pool,
                    lu_source.buy_num as source_buy_num,
                    lu_target.sub_id as target_sub_id,
                    lu_target.buy_num as target_buy_num
                FROM legacy."user" lu_source
                LEFT JOIN legacy."user" lu_target ON (
                    lu_target.user_id = target_user_id 
                    AND lu_target.pool = lu_source.pool
                )
                WHERE lu_source.user_id = source_user_id
            LOOP
                source_sub_id := pool_record.source_sub_id;
                target_sub_id := pool_record.target_sub_id;
                
                -- Check if target user already has this pool
                IF target_sub_id IS NOT NULL THEN
                    -- POOL CONFLICT: Both users have the same pool - merge via DataImpulse API
                    
                    -- Get current balance from DataImpulse for source subuser
                    BEGIN
                        SELECT (dataimpulse.subuser_balance_get(source_sub_id)->>'balance')::BIGINT 
                        INTO source_balance_bytes;
                        
                        -- Convert bytes to GB (1 GB = 1024^3 bytes), round up
                        source_balance_gb := CEIL(source_balance_bytes::NUMERIC / (1024^3));
                        
                        -- Only transfer if there's actual balance
                        IF source_balance_gb > 0 THEN
                            -- Drop balance from source subuser
                            PERFORM dataimpulse.subuser_balance_drop(source_sub_id, source_balance_gb);

                            -- Add balance to target subuser
                            PERFORM dataimpulse.subuser_balance_add(target_sub_id, source_balance_gb);
                            
                            total_transferred := total_transferred + source_balance_gb;
                            _operation_count := _operation_count + 1;
                            RETURN QUERY SELECT 'BALANCE_TRANSFER'::TEXT, 'dataimpulse_api'::TEXT, source_balance_gb::BIGINT, 
                                format('Transferred %s GB from subuser %s to %s (pool: %s)', 
                                       source_balance_gb, source_sub_id, target_sub_id, pool_record.pool);
                        END IF;
                        
                    EXCEPTION WHEN OTHERS THEN
                        -- Log error but continue with merge
                        RETURN QUERY SELECT 'ERROR'::TEXT, 'dataimpulse_balance_transfer'::TEXT, 0::BIGINT, 
                            format('Failed to transfer balance for subuser %s: %s', source_sub_id, SQLERRM);
                    END;
                    
                    -- Merge buy_num for this pool
                    UPDATE legacy."user" 
                    SET buy_num = buy_num + pool_record.source_buy_num
                    WHERE user_id = target_user_id 
                    AND pool = pool_record.pool;
                    
                    pools_merged := pools_merged + 1;
                    _operation_count := _operation_count + 1;
                    RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.user'::TEXT, 1::BIGINT, 
                        format('Merged buy_num for pool %s (added %s)', pool_record.pool, pool_record.source_buy_num);
                        
                ELSE
                    -- NO CONFLICT: Target user doesn't have this pool - simple transfer
                    UPDATE legacy."user" 
                    SET user_id = target_user_id 
                    WHERE user_id = source_user_id 
                    AND pool = pool_record.pool;
                    
                    pools_transferred := pools_transferred + 1;
                    
                END IF;
                
            END LOOP;
            
            -- Summary of pool operations
            IF pools_transferred > 0 THEN
                _operation_count := _operation_count + 1;
                RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.user'::TEXT, pools_transferred::BIGINT, 
                    format('Transferred %s unique pool records', pools_transferred);
            END IF;
            
            -- Delete any remaining source user records (those that had conflicts)
            DELETE FROM legacy."user" 
            WHERE user_id = source_user_id;
            GET DIAGNOSTICS _rows_affected = ROW_COUNT;
            IF _rows_affected > 0 THEN
                _operation_count := _operation_count + 1;
                RETURN QUERY SELECT 'DELETE'::TEXT, 'legacy.user'::TEXT, _rows_affected, 
                    format('Removed %s source pool records after balance transfer', _rows_affected);
            END IF;
            
            -- Final summary of DataImpulse operations
            IF total_transferred > 0 THEN
                _operation_count := _operation_count + 1;
                RETURN QUERY SELECT 'SUMMARY'::TEXT, 'dataimpulse_balance_total'::TEXT, total_transferred::BIGINT, 
                    format('Total balance transferred: %s GB across %s pools', total_transferred, pools_merged);
            END IF;
            
        END;
        
        -- Update promocodes created by source user
        UPDATE legacy.promocode 
        SET created_by = target_user_id 
        WHERE created_by = source_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.promocode'::TEXT, _rows_affected, 'Transferred created promocodes';
        
        -- 3. RESELLER SCHEMA TABLES
        
        -- Merge reseller records
        UPDATE reseller_inner.resellers 
        SET user_id = target_user_id 
        WHERE user_id = source_user_id
        AND NOT EXISTS (
            SELECT 1 FROM reseller_inner.resellers 
            WHERE user_id = target_user_id
        );
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'UPDATE'::TEXT, 'reseller_inner.resellers'::TEXT, _rows_affected, 'Transferred reseller records';
        
        -- Delete any remaining source reseller records (in case of conflicts)
        DELETE FROM reseller_inner.resellers 
        WHERE user_id = source_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        IF _rows_affected > 0 THEN
            _operation_count := _operation_count + 1;
            RETURN QUERY SELECT 'DELETE'::TEXT, 'reseller_inner.resellers'::TEXT, _rows_affected, 'Removed duplicate reseller records';
        END IF;
        
        -- 4. Update referral relationships in legacy.all_user
        
        -- Update users who were invited by source user
        UPDATE legacy.all_user 
        SET invited_by = target_user_id 
        WHERE invited_by = source_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.all_user'::TEXT, _rows_affected, 'Updated referral relationships (invited_by)';
        
        -- 5. MERGE USER PROFILE DATA

        SELECT email, password FROM legacy.all_user WHERE user_id = source_user_id INTO _source_user_email, _source_user_password;

        UPDATE legacy.all_user
        SET email = null, password = null
        WHERE user_id = source_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.all_user'::TEXT, _rows_affected, 'Deleted source user email and password';
        
        -- Merge key profile information (preserve target user's primary data, but merge financial data)
        UPDATE legacy.all_user 
        SET 
            ref_balance_usd = ref_balance_usd + COALESCE((
                SELECT ref_balance_usd 
                FROM legacy.all_user 
                WHERE user_id = source_user_id
            ), 0),
            -- Keep other fields from target user but could optionally merge more fields here
            email = _source_user_email,
            password = _source_user_password,
            updated_at = NOW()
        WHERE user_id = target_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'UPDATE'::TEXT, 'legacy.all_user'::TEXT, _rows_affected, 'Merged financial data (ref_balance_usd)';
        
        -- 6. FINAL CLEANUP - Delete source user
        
        DELETE FROM legacy.all_user 
        WHERE user_id = source_user_id;
        GET DIAGNOSTICS _rows_affected = ROW_COUNT;
        _operation_count := _operation_count + 1;
        RETURN QUERY SELECT 'DELETE'::TEXT, 'legacy.all_user'::TEXT, _rows_affected, 'Deleted source user profile';
        
        -- Final summary
        RETURN QUERY SELECT 'SUMMARY'::TEXT, 'merge_complete'::TEXT, _operation_count::BIGINT, 
            format('Successfully merged user %s into %s with %s operations', source_user_id, target_user_id, _operation_count);
            
    EXCEPTION WHEN OTHERS THEN
        RAISE EXCEPTION 'Error during user merge: %', SQLERRM;
    END;
END;
$$;


ALTER FUNCTION common.merge_users(source_user_id text, target_user_id text, dry_run boolean) OWNER TO postgres;

--
-- Name: password_recovery_email_html(text, text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.password_recovery_email_html(username text, recovery_url text) RETURNS text
    LANGUAGE plpgsql
    AS $_$
BEGIN
  RETURN format($html$
    <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #ffffff; border-radius: 8px; border: 1px solid #e0e0e0;">
      <div style="text-align: center; margin-bottom: 30px;">
        <h1 style="color: #1a1a1a; font-size: 24px; margin-bottom: 10px;">Recover Your Password</h1>
      </div>
    
      <div style="color: #4a4a4a; font-size: 16px; line-height: 1.6; margin-bottom: 30px;">
        <p>Hello, %s</p>
        <p>Thank you for sticking with VProxy. To recover your password, please click the button below:</p>
      </div>
    
      <div style="text-align: center; margin-bottom: 30px;">
        <a href="%s" style="background-color: #f59e0b; color: #ffffff; padding: 12px 24px; border-radius: 4px; text-decoration: none; font-weight: bold; display: inline-block;">
            Change password
        </a>
      </div>
    
      <div style="color: #666666; font-size: 14px; border-top: 1px solid #e0e0e0; padding-top: 20px;">
        <p>If you didn't request the recovery, please ignore this email.</p>
        <p>If you're having trouble clicking the button, copy and paste this link into your browser:</p>
        <p style="word-break: break-all; color: #0066cc;">%s</p>
      </div>
    </div>
  $html$, username, recovery_url, recovery_url);
END
$_$;


ALTER FUNCTION common.password_recovery_email_html(username text, recovery_url text) OWNER TO postgres;

--
-- Name: prevent_created_at_update(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.prevent_created_at_update() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
BEGIN
    IF NEW.created_at IS DISTINCT FROM OLD.created_at THEN
        RAISE EXCEPTION 'Cannot modify created_at field';
    END IF;
    RETURN NEW;
END;
$$;


ALTER FUNCTION common.prevent_created_at_update() OWNER TO postgres;

--
-- Name: prevent_modifications(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.prevent_modifications() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
BEGIN
    RAISE EXCEPTION 'Cannot modify or delete records in this table.';
    RETURN NULL;
END;
$$;


ALTER FUNCTION common.prevent_modifications() OWNER TO postgres;

--
-- Name: process_referral_payment(text, text, numeric); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.process_referral_payment(buyer_id text, order_id text, payment_amount numeric) RETURNS void
    LANGUAGE plpgsql
    AS $_$
DECLARE
    referrer_id TEXT;
    referral_percent FLOAT;
    referral_reward NUMERIC;
    current_ref_balance NUMERIC;
    v_log_source TEXT := common.build_log_source(ARRAY['common', 'process_referral_payment']);
BEGIN
    RAISE DEBUG '% buyer_id: %, order_id: %, payment_amount: %', v_log_source, buyer_id, order_id, payment_amount;
    
    -- Get referrer info and percentage
    SELECT au.invited_by, COALESCE(
           CASE 
               WHEN au.pers_percent IS NOT NULL AND au.pers_percent != '' 
               THEN au.pers_percent::float 
               ELSE NULL 
           END,
           (SELECT value::float FROM legacy.system WHERE key = 'ref_percent')
       )
    FROM legacy.all_user au
    WHERE au.user_id = buyer_id
    INTO referrer_id, referral_percent;

    -- Checks if the referrer is a actual user
    IF referrer_id IS NOT NULL AND (referrer_id ~ '^\d+$' OR referrer_id LIKE 'vgate\_%') THEN
        referral_reward := payment_amount * referral_percent;

        RAISE LOG '% apply referral rewards to user %: %', v_log_source, referrer_id, referral_reward;
        
        UPDATE legacy.all_user
        SET ref_balance_usd = ref_balance_usd + referral_reward
        WHERE user_id = referrer_id;

        INSERT INTO legacy.referral_payment_checks (
            payment_date,
            order_id,
            buyer,
            buyer_id,
            referrer,
            referrer_id,
            payment_amount,
            referral_percent,
            should_receive,
            payment_method,
            current_referral_balance,
            returned
        )
        SELECT 
            NOW(),
            p.order_id,
            buyer.username,
            buyer_id,
            ref.username,
            referrer_id,
            payment_amount,
            referral_percent * 100,
            referral_reward,
            p.service,
            current_ref_balance + referral_reward,
            true
        FROM legacy.payment p
        LEFT JOIN legacy.all_user buyer ON buyer.user_id = buyer_id
        LEFT JOIN legacy.all_user ref ON ref.user_id = referrer_id
        WHERE p.order_id = process_referral_payment.order_id;
    END IF;
END
$_$;


ALTER FUNCTION common.process_referral_payment(buyer_id text, order_id text, payment_amount numeric) OWNER TO postgres;

--
-- Name: process_successful_payment(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.process_successful_payment(o_id text) RETURNS void
    LANGUAGE plpgsql
    AS $$
DECLARE
    su_id INT;
    u_id TEXT;
    pool TEXT;
    data JSONB;
    payment_amount NUMERIC;
    purchase_source TEXT;
    v_log_source TEXT := common.build_log_source(ARRAY['common', 'process_successful_payment']);
BEGIN
    RAISE LOG '% o_id: %', v_log_source, o_id;

    -- Get payment and pool info in one query
    SELECT p.subuser_id, p.user_id, p.paid, i.pool
    INTO su_id, u_id, payment_amount, pool
    FROM legacy.payment p
    JOIN legacy.invoice i ON p.order_id = i.id
    WHERE p.order_id = o_id;
    
    IF NOT FOUND THEN
        RAISE EXCEPTION 'No payment or invoice found for order_id=%', o_id;
    END IF;

    -- Handle subuser creation if needed
    IF su_id IS NULL THEN
        su_id = dataimpulse.create_subuser(
            label := 'payment_' || u_id,
            pool_type := pool::DATAIMPULSEPOOL
        )->>'id';

        RAISE LOG '% Created new subuser: sub_id=%, pool=%', v_log_source, su_id, pool;

        INSERT INTO legacy.user (
            user_id,
            sub_id,
            pool,
            buy_num
        )
        VALUES (
            u_id,
            su_id,
            pool::DATAIMPULSEPOOL,
            (
                SELECT (COALESCE(SUM(u.buy_num), 0))
                FROM legacy.user u
                WHERE u.user_id = u_id
            )
        );

        RAISE LOG '% Inserted new user: user_id=%', v_log_source, u_id;

        UPDATE legacy.payment
        SET subuser_id = su_id,
            balance_before = '0 bytes'
        WHERE order_id = o_id;
    END IF;

    -- Update buy count
    UPDATE legacy.user
    SET buy_num = buy_num + 1
    WHERE user_id = u_id;

    RAISE LOG '% Updated buy count for user_id=%', v_log_source, u_id;

    -- Add traffic
    PERFORM dataimpulse.subuser_balance_add(
        su_id,
        (
            SELECT i.amount
            FROM legacy.invoice i
            WHERE i.id = o_id
        )
    );

    
    RAISE LOG '% Added traffic for subuser_id=% from order_id=%', v_log_source, su_id, o_id;

    -- Process referral payment
    PERFORM common.process_referral_payment(u_id, o_id, payment_amount);

    RAISE LOG '% Processed referral payment: user_id=%, order_id=%, amount=%', v_log_source, u_id, o_id, payment_amount;

    -- Prepare notification data
    SELECT jsonb_build_object(
        'user_id', p.user_id,
        'amount', p.amount_gb,
        'price', pr.gb_cost_usd * p.amount_gb,
        'order_id', p.order_id,
        'balance_before', p.balance_before,
        'discount', p.discount,
        'ref_rub', (i.id IS NULL),
        'service', p.service,
        'pool', i.pool,
        'web', (i.source = 'web')
    )
    FROM
        legacy.invoice i
    INNER JOIN
        legacy.payment p
    ON
        p.order_id = i.id
    RIGHT JOIN
        legacy.price pr
    ON
        i.pool::TEXT = pr.pool::TEXT
    WHERE
        p.order_id = o_id
    INTO
        data;

    RAISE NOTICE '% data: %', v_log_source, data;

    -- Send notification
    BEGIN
        SELECT source FROM legacy.invoice WHERE id = o_id
        INTO purchase_source;

        PERFORM common.send_purchase_notification(
            data->>'order_id',
            data->>'user_id',
            (data->>'amount')::INT,
            (data->>'price')::NUMERIC,
            (data->>'discount')::NUMERIC,
            data->>'service',
            (data->>'balance_before')::TEXT,
            purchase_source,
            (data->>'pool')::common.dataimpulsepool
        );

        -- PERFORM http_post(
        --     format('http://localhost:8000/external_buy_alert'),
        --     data::VARCHAR,
        --     'application/json'
        -- );
    EXCEPTION WHEN OTHERS THEN
        RAISE WARNING '% Failed to send notification: %', v_log_source, SQLERRM;
    END;
END
$$;


ALTER FUNCTION common.process_successful_payment(o_id text) OWNER TO postgres;

--
-- Name: remove_all_unpaired_translations(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.remove_all_unpaired_translations() RETURNS void
    LANGUAGE plpgsql
    AS $$
DECLARE
    rus_lang_id INTEGER;
    eng_lang_id INTEGER;
BEGIN
    SELECT language_id INTO rus_lang_id FROM common.get_language_id('rus');
    SELECT language_id INTO eng_lang_id FROM common.get_language_id('eng');

    DELETE FROM vgate.translation t
    WHERE t.language_id IN (rus_lang_id, eng_lang_id)
      AND t.text_id IN (
          SELECT text_id
          FROM vgate.translation
          WHERE language_id IN (rus_lang_id, eng_lang_id)
          GROUP BY text_id
          HAVING COUNT(DISTINCT language_id) < 2
      );
END;
$$;


ALTER FUNCTION common.remove_all_unpaired_translations() OWNER TO postgres;

--
-- Name: secure_hash_equals(bytea, bytea); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.secure_hash_equals(hash1 bytea, hash2 bytea) RETURNS boolean
    LANGUAGE plpgsql IMMUTABLE STRICT
    AS $$
DECLARE
    result BOOLEAN := TRUE;
    i INT;
    length1 INT := COALESCE(LENGTH(hash1), 0);
    length2 INT := COALESCE(LENGTH(hash2), 0);
    diff INT := length1 # length2; -- XOR to include length mismatch in comparison
BEGIN
    -- If lengths are different, still process to prevent timing attacks
    FOR i IN 1 .. GREATEST(length1, length2) LOOP
        diff := diff | (get_byte(hash1, i - 1) # get_byte(hash2, i - 1)); -- XOR each byte
    END LOOP;

    -- If the diff is 0, the hashes are equal
    IF diff = 0 THEN
        RETURN TRUE;
    ELSE
        RETURN FALSE;
    END IF;
END;
$$;


ALTER FUNCTION common.secure_hash_equals(hash1 bytea, hash2 bytea) OWNER TO postgres;

--
-- Name: send_backup_to_admins(text, text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.send_backup_to_admins(user_ids text DEFAULT NULL::text, telegram_tokens text DEFAULT NULL::text) RETURNS void
    LANGUAGE plsh
    AS $_$

#!/bin/sh

set -x

# send_message() {
#   /run/current-system/sw/bin/curl --verbose "https://api.telegram.org/bot$token/sendMessage" \
#     --data-urlencode 'text=${1}' \
#     --data-urlencode "chat_id=$user_id" 2>/tmp/eblan
# } 

send_document() {
  /run/current-system/sw/bin/curl https://api.telegram.org/bot${token}/sendDocument \
    -F "document=@-;filename=$(date --iso-8601=seconds).sql" \
    -Fchat_id=$user_id
}

user_ids="${1}"
telegram_tokens="${2}"

if [ -z "${telegram_tokens}" ]; then
  telegram_tokens="$(psql -Atc "SELECT common.get_setting('app.telegram_token')")"
fi

if [ -z "${user_ids}" ]; then
  user_ids="$(psql -At -c "select user_id from legacy.all_user where \"role\"='vgate_admin'")"
fi

for token in $(printf '%s' "${telegram_tokens}" | xargs -d','); do
  for user_id in $(printf '%s' "${user_ids}" | xargs -d','); do
    pg_dump | send_document
  done
done

$_$;


ALTER FUNCTION common.send_backup_to_admins(user_ids text, telegram_tokens text) OWNER TO postgres;

--
-- Name: send_low_traffic_notifications(bigint, text[]); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.send_low_traffic_notifications(min_amount_mb bigint DEFAULT 100, user_id_whitelist text[] DEFAULT NULL::text[]) RETURNS void
    LANGUAGE plpgsql
    AS $_$
DECLARE
  "response"              JSONB   := dataimpulse.all_subusers();
  "min_bytes"             BIGINT  := 1024::BIGINT*1024::BIGINT*"min_amount_mb";
  "all_users"             JSONB[] := ARRAY[]::TEXT[];
  "user_message"          common.user_notification;
  "user_messages_to_send" common.user_notification[];
BEGIN

  -- INFO: 
  SELECT array_agg(json_build_object(
    'user_id',        "user"."user_id",
    'balance',        "subuser"->>'balance',
    'balance_format', "subuser"->>'balance_format',
    'pool_type',      "subuser"->>'pool_type'
  ))
  FROM jsonb_array_elements("response") AS "subuser"
  INNER JOIN "legacy"."user" ON "legacy"."user"."sub_id" = ("subuser"->>'id')::bigint
  WHERE
    "user_id_whitelist" IS NULL
    OR "user"."user_id"::TEXT = ANY("user_id_whitelist")
  INTO "all_users";
  
  -- INFO: Cleanup those who have more than min_bytes since the last time
  WITH "user_ok" AS (
    SELECT "user"->>'user_id' AS "user_id"
    FROM unnest("all_users") AS "user"
    GROUP BY "user"->>'user_id'
    HAVING bool_and(("user"->>'balance')::numeric > "min_bytes")
  )
  UPDATE "legacy"."all_user"
  SET "notified_low_traffic" = false
  FROM user_ok
  WHERE
    "legacy"."all_user"."notified_low_traffic"
    AND "legacy"."all_user"."user_id" = "user_ok"."user_id"
  ;

  -- INFO: Aggregate pools of each user into a single message per user
  WITH formatted_messages AS (
    SELECT
      "_user"->>'user_id' AS user_id,
      row(
        common.indoc($html$
          Warning! Traffic on one of your pool will soon run out!
          Pools:
          %s
        $html$),
        ARRAY[
	  string_agg(
	    format(
	      '⚫️ %s: %s',
	      _user->>'pool_type',
	      _user->'balance_format'
            ),
	    E'\n')
	]::TEXT[]
      )::common.format_promise AS message_text
    FROM unnest("all_users") AS "_user"
    WHERE ("_user"->>'balance')::bigint <= min_bytes::bigint
    GROUP BY "_user"->>'user_id'
  )
  SELECT array_agg(row(user_id, message_text))
  INTO "user_messages_to_send"
  FROM formatted_messages;

  -- RAISE NOTICE 'user_messages_to_send: %', user_messages_to_send;

  IF array_length("user_messages_to_send", 1) > 0 THEN
    FOREACH "user_message" IN ARRAY "user_messages_to_send" LOOP
      CONTINUE WHEN EXISTS(
        SELECT *
        FROM "legacy"."all_user"
        WHERE "all_user"."notified_low_traffic"
          AND "all_user"."user_id" = "user_message"."user_id"
      );

      UPDATE "legacy"."all_user"
      SET "notified_low_traffic" = true
      WHERE "all_user"."user_id" = "user_message"."user_id"
      ;

      RAISE NOTICE 'Sending to user: % text: %',
        "user_message"."user_id",
        "user_message"."message"
      ;
      
      --PERFORM FROM common.send_telegram_message(
      --  user_ids         := ARRAY["user_message"->>'user_id']::TEXT[],
      --  message_format   := ROW("user_message"->>'message_text', ARRAY[]::TEXT[])::common.format_promise,
      --  inline_buttons   := null::text[],
      --  message_language := 'eng'::vgate.language,
      --  telegram_tokens  := telegram_tokens::text[]
      --);

      INSERT INTO legacy.notification_pool (
	target_users,
	message,
	message_language,
	type
      ) VALUES
      ( 
        ARRAY["user_message"."user_id"]::TEXT[],
        "user_message"."message"::common.format_promise,
	'eng'::vgate.language,
	'low balance'
       );

    END LOOP;
  END IF;
END;
$_$;


ALTER FUNCTION common.send_low_traffic_notifications(min_amount_mb bigint, user_id_whitelist text[]) OWNER TO postgres;

--
-- Name: send_promo_notification(text, text, integer, text, text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.send_promo_notification(user_id text, promocode text, amount integer, balance_before text, source text) RETURNS void
    LANGUAGE plpgsql
    AS $_$
BEGIN
    IF user_id LIKE 'vgate_%' THEN
        user_id := substring(user_id FROM 7);
    END IF;

    PERFORM common.send_telegram_message(
        user_ids         := common.admins(),
        message_format   := row(common.indoc($html$
                <b>Пользователь</b> <code>%s</code> <b>активировал промокод "%s"</b>
                <b>Баланс до использования:</b> <code>%s</code>
                <b>Добавлено:</b> <code>%s</code> <b>GB</b>
                <b>Источник:</b> <code>%s</code>
            $html$::TEXT),
            ARRAY[
                user_id,
                replace(promocode, '"', ''),
                replace(balance_before, '"', ''),
                amount,
                CASE source WHEN 'bot' THEN '🤖 тг-бот' WHEN 'web' THEN '�� веб-сайт' END
            ]::TEXT[]
        )::common.format_promise
     );
END
$_$;


ALTER FUNCTION common.send_promo_notification(user_id text, promocode text, amount integer, balance_before text, source text) OWNER TO postgres;

--
-- Name: send_purchase_notification(text, text, integer, numeric, numeric, text, text, text, common.dataimpulsepool); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.send_purchase_notification(order_id text, user_id text, amount integer, price numeric, discount numeric, service text, balance_before text, source text, pool common.dataimpulsepool) RETURNS void
    LANGUAGE plpgsql
    AS $_$
DECLARE
    discount_text TEXT := '';
    message_format common.format_promise := row(common.indoc($html$
            ✅ Платеж успешно выполнен!
            
            Тип прокси: <code>%s</code>
            Количество: <code>%s</code> GB
            
            Трафик добавлен на ваш баланс.
        $html$::TEXT),
        ARRAY[
            pool,
            amount
        ]::TEXT[]
    )::common.format_promise;
    user_ids TEXT[];
BEGIN
    IF order_id IS NULL THEN
        order_id := '';
    ELSE
        order_id := '#' || order_id;
    END IF;

    IF discount > 0 THEN
        discount_text := format('<b>Скидка: %s%%</b>', round(discount * 100, 1));
    END IF;

    user_ids := ARRAY[user_id]::TEXT[];

    -- NOTE: legacy notifications directly via telegram
    IF user_id LIKE 'vgate_%' THEN
        user_id := substring(user_id FROM 7);
        -- TODO(yukkop): add web user notification
    ELSE
        -- Telegram user notification
        PERFORM common.send_telegram_message(
            user_ids         := user_ids,
            message_format   := message_format,
            inline_buttons   := ARRAY[
                ARRAY['🔄 Обновить профиль', '"update_profile"']
            ]
        );
    END IF;

    --NOTE: new notification realization via notification queue
    INSERT INTO legacy.notification_pool (
      target_users,
      message,
      message_language,
      type
    ) VALUES
    ( 
      user_ids,
      message_format,
      'rus'::vgate.language,
      'purchase completed'
    );


    -- NOTE: admin notification still always via telegram
    PERFORM common.send_telegram_message(
        user_ids         := common.admins(),
        message_format   := row(common.indoc($html$
                <b>Новая покупка!</b>
                <b>Пользователь</b> <code>%s</code> купил <code>%s</code>GB за <code>%s</code>$
                <b>Баланс до покупки:</b> <code>%s</code>
                <b>Источник: %s</b>
                <b>Способ оплаты: <code>%s</code> <code>%s</code></b>
            $html$::TEXT),
            ARRAY[
                user_id,
                amount,
                price,
                replace(balance_before, '"', ''),
                CASE source WHEN 'bot' THEN '🤖 тг-бот' WHEN 'web' THEN '🌐 веб-сайт' END,
                service,
                order_id
            ]::TEXT[]
        )::common.format_promise
     );
END
$_$;


ALTER FUNCTION common.send_purchase_notification(order_id text, user_id text, amount integer, price numeric, discount numeric, service text, balance_before text, source text, pool common.dataimpulsepool) OWNER TO postgres;

--
-- Name: send_signup_notification(text, text, text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.send_signup_notification(id text, username text, source text) RETURNS void
    LANGUAGE plpgsql
    AS $_$
BEGIN
    PERFORM common.send_telegram_message(
        user_ids         := common.admins(),
        message_format   := row(common.indoc($html$
                <b>Новый пользователь!</b>
                <b>ID:</b> <code>%s</code>
                <b>Юзернейм:</b> <code>%s</code>
                <b>Источник: %s</b>
            $html$::TEXT),
            ARRAY[
                id,
                username,
                CASE source WHEN 'bot' THEN '🤖 тг-бот' WHEN 'web' THEN '🌐 веб-сайт' END
            ]::TEXT[]
        )::common.format_promise
     );
END
$_$;


ALTER FUNCTION common.send_signup_notification(id text, username text, source text) OWNER TO postgres;

--
-- Name: send_telegram_message(text[], common.format_promise, text[], vgate.language, text[]); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.send_telegram_message(user_ids text[], message_format common.format_promise, inline_buttons text[] DEFAULT NULL::text[], message_language vgate.language DEFAULT 'rus'::vgate.language, telegram_tokens text[] DEFAULT '{}'::text[]) RETURNS TABLE(user_id text, success boolean, http_status integer, error_message text, bot_token_used text, response_content text)
    LANGUAGE plpgsql
    AS $_$
DECLARE
    message_text TEXT;
    bot_token TEXT;
    bot_tokens TEXT[];
    telegram_url TEXT;
    post_data JSONB;
    current_user_id TEXT;
    message_hash TEXT;
    inline_keyboard JSONB;
    button_row text[];
    keyboard_row JSONB;
    target_language vgate.language;
BEGIN
    IF telegram_tokens = '{}' THEN
        SELECT string_to_array(common.get_setting('app.telegram_token'), ',') INTO bot_tokens;
    ELSE
        bot_tokens := telegram_tokens;
    END IF;
    
    IF bot_tokens = '{}' THEN
        RAISE EXCEPTION 'Telegram bot token not configured in vinn.storage table';
    END IF;

    -- Build inline keyboard if buttons provided
    IF inline_buttons IS NOT NULL AND array_length(inline_buttons, 1) > 0 THEN
        inline_keyboard := '[]'::jsonb;
        FOREACH button_row SLICE 1 IN ARRAY inline_buttons LOOP
            IF array_length(button_row, 1) >= 2 THEN
                keyboard_row := jsonb_build_array(
                    jsonb_build_object(
                        'text', button_row[1],
                        'callback_data', button_row[2]
                    )
                );
                inline_keyboard := inline_keyboard || jsonb_build_array(keyboard_row);
            END IF;
        END LOOP;
    END IF;

    SELECT encode(digest(message_format.text, 'sha256'), 'hex') INTO message_hash;

    RAISE LOG 'count of users: %', array_length(user_ids, 1);
    RAISE LOG 'count of bot_tokens: %', array_length(bot_tokens, 1);

    FOREACH current_user_id IN ARRAY user_ids LOOP

        user_id := current_user_id;
        bot_token_used := bot_token;

        SELECT lang::vgate.language FROM legacy.all_user WHERE legacy.all_user.user_id = current_user_id INTO target_language;

        message_text := format(vgate.translate(message_hash::common.sha256, 'notification', message_format.text, message_language, target_language), VARIADIC message_format.args);
    
        FOREACH bot_token IN ARRAY bot_tokens LOOP
            success := false;

            IF common.is_numeric(current_user_id) THEN
                telegram_url := format('https://api.telegram.org/bot%s/sendMessage', bot_token);
                
                post_data := jsonb_build_object(
                    'chat_id', current_user_id,
                    'text', message_text,
                    'parse_mode', 'HTML'
                );

                -- Add inline markup if provided
                IF inline_buttons IS NOT NULL THEN
                    post_data := post_data || jsonb_build_object(
                        'reply_markup', 
                        jsonb_build_object('inline_keyboard', inline_keyboard)
                    );
                END IF;

                DECLARE
                    http_response http_response;
                BEGIN
                    RAISE DEBUG $sh$curl -X POST % -d '%' -H "Content-Type: application/json"$sh$, telegram_url, post_data::TEXT;

                    SELECT * FROM http_post(
                        telegram_url,
                        post_data::TEXT,
                        'application/json'
                    ) INTO http_response;
                    
                    IF http_response.status >= 200 AND http_response.status < 300 THEN
                        RAISE NOTICE 'Telegram message sent to user %', current_user_id;
                        
                        success := true;
                        error_message := NULL;
                    ELSE
                        RAISE WARNING 'Telegram API error for user %: HTTP % - %', 
                            current_user_id, http_response.status, http_response.content;
                            
                        error_message := 'HTTP ' || http_response.status || ': ' || http_response.content;
                    END IF;

                    http_status := http_response.status;
                    response_content := http_response.content;
                    RETURN NEXT;
                    
                EXCEPTION WHEN OTHERS THEN
                    RAISE WARNING 'Failed to send Telegram message to user %: %', 
                        current_user_id, SQLERRM;
                        
                    -- Return exception row
                    http_status := NULL;
                    error_message := SQLERRM;
                    response_content := NULL;
                    RETURN NEXT;
                END;
            ELSE
                RAISE WARNING 'No telegram_id found for user_id: %', current_user_id;
                
                -- Return invalid user_id row
                http_status := NULL;
                error_message := 'Invalid user_id: not numeric';
                response_content := NULL;
                RETURN NEXT;
            END IF;
        END LOOP;
    END LOOP;
END;
$_$;


ALTER FUNCTION common.send_telegram_message(user_ids text[], message_format common.format_promise, inline_buttons text[], message_language vgate.language, telegram_tokens text[]) OWNER TO postgres;

--
-- Name: send_telegram_message_2(text[], common.format_promise, text[], vgate.language, text[], text, text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.send_telegram_message_2(user_ids text[], message_format common.format_promise, inline_buttons text[] DEFAULT NULL::text[], message_language vgate.language DEFAULT 'rus'::vgate.language, telegram_tokens text[] DEFAULT '{}'::text[], image_url text DEFAULT NULL::text, parse_mode text DEFAULT 'HTML'::text) RETURNS TABLE(user_id text, success boolean, http_status integer, error_message text, bot_token_used text, response_content text)
    LANGUAGE plpgsql
    AS $_$
DECLARE
    message_text TEXT;
    bot_token TEXT;
    bot_tokens TEXT[];
    telegram_url TEXT;
    post_data JSONB;
    current_user_id TEXT;
    message_hash TEXT;
    inline_keyboard JSONB;
    button_row text[];
    keyboard_row JSONB;
    target_language vgate.language;
    filtered_user_ids TEXT[];
    blocked_user_ids TEXT[];
BEGIN
    IF telegram_tokens = '{}' THEN
        SELECT string_to_array(common.get_setting('app.telegram_token'), ',') INTO bot_tokens;
    ELSE
        bot_tokens := telegram_tokens;
    END IF;
    
    IF bot_tokens = '{}' THEN
        RAISE EXCEPTION 'Telegram bot token not configured in vinn.storage table';
    END IF;

    -- Filter out users who have already blocked the bot
    SELECT ARRAY(
        SELECT unnest(user_ids) 
        EXCEPT 
        SELECT au.user_id 
        FROM legacy.all_user au 
        WHERE au.user_id = ANY(user_ids) 
        AND au.telegram_blocked = TRUE
    ) INTO filtered_user_ids;

    -- Get list of blocked users for reporting
    SELECT ARRAY(
        SELECT au.user_id 
        FROM legacy.all_user au 
        WHERE au.user_id = ANY(user_ids) 
        AND au.telegram_blocked = TRUE
    ) INTO blocked_user_ids;

    -- Return entries for already blocked users
    FOREACH current_user_id IN ARRAY blocked_user_ids LOOP
        user_id := current_user_id;
        success := false;
        http_status := 403;
        error_message := 'User has previously blocked the bot';
        bot_token_used := NULL;
        response_content := NULL;
        RETURN NEXT;
    END LOOP;

    -- Build inline keyboard if buttons provided
    IF inline_buttons IS NOT NULL AND array_length(inline_buttons, 1) > 0 THEN
        inline_keyboard := '[]'::jsonb;
        FOREACH button_row SLICE 1 IN ARRAY inline_buttons LOOP
            IF array_length(button_row, 1) >= 2 THEN
                keyboard_row := jsonb_build_array(
                    jsonb_build_object(
                        'text', button_row[1],
                        'callback_data', button_row[2]
                    )
                );
                inline_keyboard := inline_keyboard || jsonb_build_array(keyboard_row);
            END IF;
        END LOOP;
    END IF;

    SELECT encode(digest(message_format.text, 'sha256'), 'hex') INTO message_hash;

    RAISE LOG 'total users: %, filtered users: %, blocked users: %', 
        array_length(user_ids, 1), 
        array_length(filtered_user_ids, 1), 
        array_length(blocked_user_ids, 1);
    RAISE LOG 'count of bot_tokens: %', array_length(bot_tokens, 1);

    FOREACH current_user_id IN ARRAY filtered_user_ids LOOP

        user_id := current_user_id;
        bot_token_used := bot_token;

        SELECT lang::vgate.language FROM legacy.all_user WHERE legacy.all_user.user_id = current_user_id INTO target_language;

        message_text := format(vgate.translate(message_hash::common.sha256, 'notification', message_format.text, message_language, target_language), VARIADIC message_format.args);
    
        FOREACH bot_token IN ARRAY bot_tokens LOOP
            success := false;

            IF common.is_numeric(current_user_id) THEN
                -- Choose API endpoint based on whether image is provided
                IF image_url IS NOT NULL AND image_url != '' THEN
                    telegram_url := format('https://api.telegram.org/bot%s/sendPhoto', bot_token);
                    
                    post_data := jsonb_build_object(
                        'chat_id', current_user_id,
                        'photo', image_url,
                        'caption', message_text,
                        'parse_mode', parse_mode
                    );
                ELSE
                    telegram_url := format('https://api.telegram.org/bot%s/sendMessage', bot_token);
                    
                    post_data := jsonb_build_object(
                        'chat_id', current_user_id,
                        'text', message_text,
                        'parse_mode', parse_mode
                    );
                END IF;

                -- Add inline markup if provided
                IF inline_buttons IS NOT NULL THEN
                    post_data := post_data || jsonb_build_object(
                        'reply_markup', 
                        jsonb_build_object('inline_keyboard', inline_keyboard)
                    );
                END IF;

                DECLARE
                    http_response http_response;
                BEGIN
                    RAISE DEBUG $sh$curl -X POST % -d '%' -H "Content-Type: application/json"$sh$, telegram_url, post_data::TEXT;

                    SELECT * FROM http_post(
                        telegram_url,
                        post_data::TEXT,
                        'application/json'
                    ) INTO http_response;
                    
                    IF http_response.status >= 200 AND http_response.status < 300 THEN
                        RAISE NOTICE 'Telegram message sent to user %', current_user_id;
                        
                        success := true;
                        error_message := NULL;
                    ELSE
                        RAISE WARNING 'Telegram API error for user %: HTTP % - %', 
                            current_user_id, http_response.status, http_response.content;
                            
                        error_message := 'HTTP ' || http_response.status || ': ' || http_response.content;
                        
                        -- Check if user has blocked the bot (403 Forbidden with specific error codes)
                        IF http_response.status = 403 AND (
                            http_response.content LIKE '%blocked%' OR
                            http_response.content LIKE '%bot was blocked%' OR
                            http_response.content LIKE '%user is deactivated%'
                        ) THEN
                            -- Mark user as blocked to prevent future attempts
                            UPDATE legacy.all_user 
                            SET telegram_blocked = TRUE, updated_at = NOW()
                            WHERE legacy.all_user.user_id = current_user_id;
                            
                            RAISE NOTICE 'User % has blocked the bot, marked as blocked', current_user_id;
                            error_message := 'User has blocked the bot (marked for future exclusion)';
                        END IF;
                    END IF;

                    http_status := http_response.status;
                    response_content := http_response.content;
                    RETURN NEXT;
                    
                EXCEPTION WHEN OTHERS THEN
                    RAISE WARNING 'Failed to send Telegram message to user %: %', 
                        current_user_id, SQLERRM;
                        
                    -- Return exception row
                    http_status := NULL;
                    error_message := SQLERRM;
                    response_content := NULL;
                    RETURN NEXT;
                END;
            ELSE
                RAISE WARNING 'No telegram_id found for user_id: %', current_user_id;
                
                -- Return invalid user_id row
                http_status := NULL;
                error_message := 'Invalid user_id: not numeric';
                response_content := NULL;
                RETURN NEXT;
            END IF;
        END LOOP;
    END LOOP;
END;
$_$;


ALTER FUNCTION common.send_telegram_message_2(user_ids text[], message_format common.format_promise, inline_buttons text[], message_language vgate.language, telegram_tokens text[], image_url text, parse_mode text) OWNER TO postgres;

--
-- Name: send_telegram_message_3(text[], common.format_promise, text[], vgate.language, text[], text, uuid); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.send_telegram_message_3(user_ids text[], message_format common.format_promise, inline_buttons text[] DEFAULT NULL::text[], message_language vgate.language DEFAULT 'rus'::vgate.language, telegram_tokens text[] DEFAULT '{}'::text[], image_url text DEFAULT NULL::text, batch_id uuid DEFAULT gen_random_uuid()) RETURNS TABLE(user_id text, success boolean, http_status integer, error_message text, bot_token_used text, response_content text)
    LANGUAGE plpgsql
    AS $_$
DECLARE
    message_text TEXT;
    bot_token TEXT;
    bot_tokens TEXT[];
    telegram_url TEXT;
    post_data JSONB;
    current_user_id TEXT;
    message_hash TEXT;
    inline_keyboard JSONB;
    button_row text[];
    keyboard_row JSONB;
    target_language vgate.language;
    filtered_user_ids TEXT[];
    blocked_user_ids TEXT[];
BEGIN
    RAISE NOTICE 'Starting telegram message batch % with % users', batch_id, array_length(user_ids, 1);

    IF telegram_tokens = '{}' THEN
        SELECT string_to_array(common.get_setting('app.telegram_token'), ',') INTO bot_tokens;
    ELSE
        bot_tokens := telegram_tokens;
    END IF;
    
    IF bot_tokens = '{}' THEN
        RAISE EXCEPTION 'Telegram bot token not configured in vinn.storage table';
    END IF;

    -- Filter out users who have already blocked the bot
    SELECT ARRAY(
        SELECT unnest(user_ids) 
        EXCEPT 
        SELECT au.user_id 
        FROM legacy.all_user au 
        WHERE au.user_id = ANY(user_ids) 
        AND au.telegram_blocked = TRUE
    ) INTO filtered_user_ids;

    -- Get list of blocked users for reporting
    SELECT ARRAY(
        SELECT au.user_id 
        FROM legacy.all_user au 
        WHERE au.user_id = ANY(user_ids) 
        AND au.telegram_blocked = TRUE
    ) INTO blocked_user_ids;

    -- Return entries for already blocked users
    FOREACH current_user_id IN ARRAY blocked_user_ids LOOP
        user_id := current_user_id;
        success := false;
        http_status := 403;
        error_message := 'User has previously blocked the bot';
        bot_token_used := NULL;
        response_content := NULL;
        RETURN NEXT;
    END LOOP;

    -- Build inline keyboard if buttons provided
    IF inline_buttons IS NOT NULL AND array_length(inline_buttons, 1) > 0 THEN
        inline_keyboard := '[]'::jsonb;
        FOREACH button_row SLICE 1 IN ARRAY inline_buttons LOOP
            IF array_length(button_row, 1) >= 2 THEN
                keyboard_row := jsonb_build_array(
                    jsonb_build_object(
                        'text', button_row[1],
                        'callback_data', button_row[2]
                    )
                );
                inline_keyboard := inline_keyboard || jsonb_build_array(keyboard_row);
            END IF;
        END LOOP;
    END IF;

    SELECT encode(digest(message_format.text, 'sha256'), 'hex') INTO message_hash;

    RAISE LOG 'total users: %, filtered users: %, blocked users: %', 
        array_length(user_ids, 1), 
        array_length(filtered_user_ids, 1), 
        array_length(blocked_user_ids, 1);
    RAISE LOG 'count of bot_tokens: %', array_length(bot_tokens, 1);

    FOREACH current_user_id IN ARRAY filtered_user_ids LOOP

        user_id := current_user_id;
        bot_token_used := bot_token;

        SELECT lang::vgate.language FROM legacy.all_user WHERE legacy.all_user.user_id = current_user_id INTO target_language;

        message_text := format(vgate.translate(message_hash::common.sha256, 'notification', message_format.text, message_language, target_language), VARIADIC message_format.args);
    
        FOREACH bot_token IN ARRAY bot_tokens LOOP
            success := false;

            IF common.is_numeric(current_user_id) THEN
                -- Choose API endpoint based on whether image is provided
                IF image_url IS NOT NULL AND image_url != '' THEN
                    telegram_url := format('https://api.telegram.org/bot%s/sendPhoto', bot_token);
                    
                    post_data := jsonb_build_object(
                        'chat_id', current_user_id,
                        'photo', image_url,
                        'caption', message_text,
                        'parse_mode', 'HTML'
                    );
                ELSE
                    telegram_url := format('https://api.telegram.org/bot%s/sendMessage', bot_token);
                    
                    post_data := jsonb_build_object(
                        'chat_id', current_user_id,
                        'text', message_text,
                        'parse_mode', 'HTML'
                    );
                END IF;

                -- Add inline markup if provided
                IF inline_buttons IS NOT NULL THEN
                    post_data := post_data || jsonb_build_object(
                        'reply_markup', 
                        jsonb_build_object('inline_keyboard', inline_keyboard)
                    );
                END IF;

                DECLARE
                    http_response http_response;
                BEGIN
                    RAISE DEBUG $sh$curl -X POST % -d '%' -H "Content-Type: application/json"$sh$, telegram_url, post_data::TEXT;

                    SELECT * FROM http_post(
                        telegram_url,
                        post_data::TEXT,
                        'application/json'
                    ) INTO http_response;
                    
                    IF http_response.status >= 200 AND http_response.status < 300 THEN
                        RAISE NOTICE 'Telegram message sent to user %', current_user_id;
                        
                        success := true;
                        error_message := NULL;
                    ELSE
                        RAISE WARNING 'Telegram API error for user %: HTTP % - %', 
                            current_user_id, http_response.status, http_response.content;
                            
                        error_message := 'HTTP ' || http_response.status || ': ' || http_response.content;
                        
                        -- Check if user has blocked the bot (403 Forbidden with specific error codes)
                        IF http_response.status = 403 AND (
                            http_response.content LIKE '%blocked%' OR
                            http_response.content LIKE '%bot was blocked%' OR
                            http_response.content LIKE '%user is deactivated%'
                        ) THEN
                            -- Mark user as blocked to prevent future attempts
                            UPDATE legacy.all_user 
                            SET telegram_blocked = TRUE, updated_at = NOW()
                            WHERE legacy.all_user.user_id = current_user_id;
                            
                            RAISE NOTICE 'User % has blocked the bot, marked as blocked', current_user_id;
                            error_message := 'User has blocked the bot (marked for future exclusion)';
                        END IF;
                    END IF;

                    http_status := http_response.status;
                    response_content := http_response.content;
                EXCEPTION WHEN OTHERS THEN
                    RAISE WARNING 'Failed to send Telegram message to user %: %', 
                        current_user_id, SQLERRM;
                        
                    -- Return exception row
                    http_status := NULL;
                    error_message := SQLERRM;
                    response_content := NULL;
                END;
            ELSE
                RAISE WARNING 'No telegram_id found for user_id: %', current_user_id;
                
                -- Return invalid user_id row
                http_status := NULL;
                error_message := 'Invalid user_id: not numeric';
                response_content := NULL;
            END IF;

            INSERT INTO legacy.telegram_message_results 
            (batch_id, user_id, success, http_status, error_message, bot_token_used, response_content, message_hash)
            VALUES
            (batch_id, user_id, success, http_status, error_message, bot_token_used, response_content, message_hash::common.sha256);
            
            RETURN NEXT;
            
            COMMIT;
        END LOOP;
    END LOOP;
END;
$_$;


ALTER FUNCTION common.send_telegram_message_3(user_ids text[], message_format common.format_promise, inline_buttons text[], message_language vgate.language, telegram_tokens text[], image_url text, batch_id uuid) OWNER TO postgres;

--
-- Name: set_setting(text, text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.set_setting(k text, v text) RETURNS void
    LANGUAGE plpgsql
    AS $$
BEGIN
  INSERT INTO "legacy"."system" ("key", "value")
  VALUES ("k", "v")
  ON CONFLICT ("key") DO UPDATE 
    SET "value" = "v"
  ;
END
$$;


ALTER FUNCTION common.set_setting(k text, v text) OWNER TO postgres;

--
-- Name: unpaired_translations(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.unpaired_translations() RETURNS TABLE(text_id common.sha256, content text, language_code character, country_code character, created_at timestamp with time zone, updated_at timestamp with time zone)
    LANGUAGE plpgsql
    AS $$
DECLARE
    rus_lang_id INTEGER;
    eng_lang_id INTEGER;
BEGIN
    SELECT a.language_id INTO rus_lang_id FROM common.get_language_id('rus') as a;
    SELECT a.language_id INTO eng_lang_id FROM common.get_language_id('eng') as a;

    RETURN QUERY
    SELECT 
        t.text_id,
        t.content,
        l.code as language_code,
        c.code as country_code,
        t.created_at,
        t.updated_at
    FROM vgate.translation t
    JOIN common.language l ON t.language_id = l.id
    JOIN common.country c ON t.country_id = c.id
    WHERE t.language_id IN (rus_lang_id, eng_lang_id)
      AND t.text_id IN (
          SELECT tt.text_id
          FROM vgate.translation as tt
          WHERE tt.language_id IN (rus_lang_id, eng_lang_id)
          GROUP BY tt.text_id
          HAVING COUNT(DISTINCT tt.language_id) < 2
      )
    ORDER BY t.text_id, t.language_id;
END;
$$;


ALTER FUNCTION common.unpaired_translations() OWNER TO postgres;

--
-- Name: unsafe.wipe_user(text); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common."unsafe.wipe_user"(target_user_id text) RETURNS void
    LANGUAGE plpgsql
    AS $$
BEGIN
    -- Remove from related legacy tables
    DELETE FROM legacy."user" WHERE user_id = target_user_id;
    DELETE FROM legacy.payment WHERE user_id = target_user_id;
    DELETE FROM legacy.request WHERE user_id = target_user_id;
    DELETE FROM legacy.invoice WHERE user_id = target_user_id;
    DELETE FROM reseller_inner.reseller_subusers
    USING reseller_inner.resellers
    WHERE reseller_subusers.reseller_id = resellers.id
      AND resellers.user_id = target_user_id;
    DELETE FROM reseller_inner.resellers WHERE user_id = target_user_id;
    DELETE FROM legacy.cross_promo_activations WHERE user_id = target_user_id;
    
    -- Remove from main table (this is what the Python bot checks)
    DELETE FROM legacy.all_user WHERE user_id = target_user_id;
    
    -- Try to drop the PostgreSQL role if it exists
    BEGIN
        EXECUTE format('DROP ROLE IF EXISTS "%s"', target_user_id);
    EXCEPTION 
        WHEN OTHERS THEN 
            RAISE NOTICE 'Could not drop role %: %', target_user_id, SQLERRM;
    END;
    
    RAISE NOTICE 'User % has been removed from legacy schema', target_user_id;
END $$;


ALTER FUNCTION common."unsafe.wipe_user"(target_user_id text) OWNER TO postgres;

--
-- Name: update_forbid_insert_to_prevent_modifications(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.update_forbid_insert_to_prevent_modifications() RETURNS void
    LANGUAGE plpgsql
    AS $$
DECLARE
    child_table RECORD;
    trigger_name TEXT;
    create_trigger_sql TEXT;
BEGIN
    FOR child_table IN
        SELECT
            c.relname AS child_table,
            ns.nspname AS schema_name
        FROM
            pg_inherits AS inh
            JOIN pg_class AS c ON inh.inhrelid = c.oid
            JOIN pg_class AS p ON inh.inhparent = p.oid
            JOIN pg_namespace AS ns ON c.relnamespace = ns.oid
        WHERE
            p.relname = 'common.forbid_insert'
    LOOP
        trigger_name := format('prevent_insert_%s', child_table.child_table);
        create_trigger_sql := format(
            'CREATE TRIGGER %I BEFORE INSERT ON %I.%I FOR EACH ROW EXECUTE FUNCTION prevent_modifications();',
            trigger_name,
            child_table.schema_name,
            child_table.child_table
        );
        EXECUTE create_trigger_sql;
    END LOOP;
END;
$$;


ALTER FUNCTION common.update_forbid_insert_to_prevent_modifications() OWNER TO postgres;

--
-- Name: update_immutable_to_prevent_modifications(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.update_immutable_to_prevent_modifications() RETURNS void
    LANGUAGE plpgsql
    AS $$
DECLARE
    child_table RECORD;
    trigger_name TEXT;
    create_trigger_sql TEXT;
BEGIN
    FOR child_table IN
        SELECT
            c.relname AS child_table,
            ns.nspname AS schema_name
        FROM
            pg_inherits AS inh
            JOIN pg_class AS c ON inh.inhrelid = c.oid
            JOIN pg_class AS p ON inh.inhparent = p.oid
            JOIN pg_namespace AS ns ON c.relnamespace = ns.oid
        WHERE
            p.relname = 'common.immutable'
    LOOP
        trigger_name := format('prevent_update_%s', child_table.child_table);
        create_trigger_sql := format(
            'CREATE TRIGGER %I BEFORE UPDATE ON %I.%I FOR EACH ROW EXECUTE FUNCTION prevent_modifications();',
            trigger_name,
            child_table.schema_name,
            child_table.child_table
        );
        EXECUTE create_trigger_sql;
        trigger_name := format('prevent_delete_%s', child_table.child_table);
        create_trigger_sql := format(
            'CREATE TRIGGER %I BEFORE DELETE ON %I.%I FOR EACH ROW EXECUTE FUNCTION prevent_modifications();',
            trigger_name,
            child_table.schema_name,
            child_table.child_table
        );
        EXECUTE create_trigger_sql;
    END LOOP;
END;
$$;


ALTER FUNCTION common.update_immutable_to_prevent_modifications() OWNER TO postgres;

--
-- Name: update_token(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.update_token() RETURNS void
    LANGUAGE plpgsql
    AS $$
BEGIN
    UPDATE vinn.storage SET dataimpulse_jwt = 'Bearer ' || dataimpulse.get_token();
END;
$$;


ALTER FUNCTION common.update_token() OWNER TO postgres;

--
-- Name: update_updated_at_column(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.update_updated_at_column() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
BEGIN
    IF NEW.updated_at IS DISTINCT FROM OLD.updated_at THEN
        RAISE EXCEPTION 'Cannot modify updated_at field';
    END IF;
    RETURN NEW;
    NEW.updated_at = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$;


ALTER FUNCTION common.update_updated_at_column() OWNER TO postgres;

--
-- Name: validate_currency_price_to_json(); Type: FUNCTION; Schema: common; Owner: postgres
--

CREATE FUNCTION common.validate_currency_price_to_json() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
DECLARE
    key TEXT;
BEGIN
    IF jsonb_typeof(NEW.to) <> 'object' THEN
        RAISE EXCEPTION 'The "to" field must be a JSON object';
    END IF;

    FOR key IN SELECT key FROM jsonb_object_keys(NEW.to) LOOP
        IF NOT EXISTS (SELECT 1 FROM currency WHERE code = key) THEN
            RAISE EXCEPTION 'Invalid currency code in "to" JSONB: %', key;
        END IF;
    END LOOP;

    RETURN NEW;
END;
$$;


ALTER FUNCTION common.validate_currency_price_to_json() OWNER TO postgres;

SET default_tablespace = '';

SET default_table_access_method = heap;

--
-- Name: created_at; Type: TABLE; Schema: common; Owner: postgres
--

CREATE TABLE common.created_at (
    created_at timestamp with time zone DEFAULT now() NOT NULL
);


ALTER TABLE common.created_at OWNER TO postgres;

--
-- Name: updated_at; Type: TABLE; Schema: common; Owner: postgres
--

CREATE TABLE common.updated_at (
    updated_at timestamp with time zone DEFAULT now() NOT NULL
);


ALTER TABLE common.updated_at OWNER TO postgres;

--
-- Name: ordered; Type: TABLE; Schema: common; Owner: postgres
--

CREATE TABLE common.ordered (
    "order" bigint NOT NULL
);


ALTER TABLE common.ordered OWNER TO postgres;

--
-- Name: forbid_insert; Type: TABLE; Schema: common; Owner: postgres
--

CREATE TABLE common.forbid_insert (
);


ALTER TABLE common.forbid_insert OWNER TO postgres;

--
-- Name: immutable; Type: TABLE; Schema: common; Owner: postgres
--

CREATE TABLE common.immutable (
);


ALTER TABLE common.immutable OWNER TO postgres;

--
-- Name: country; Type: TABLE; Schema: common; Owner: postgres
--

CREATE TABLE common.country (
    id integer NOT NULL,
    name character varying(100) NOT NULL,
    code_2 character(3) NOT NULL,
    numeric_code character(3) NOT NULL,
    code character(2) NOT NULL,
    phonecode character varying(255) NOT NULL,
    capital character varying(255) NOT NULL,
    currency character varying(255) NOT NULL,
    currency_name character varying(255) NOT NULL,
    currency_symbol character varying(255) NOT NULL,
    tld character varying(255),
    native character varying(255),
    region character varying(255),
    region_id integer,
    subregion character varying(255),
    subregion_id integer,
    nationality character varying(255),
    timezones text,
    translations text,
    latitude numeric(10,8),
    longitude numeric(11,8),
    emoji character varying(191),
    emoji_u character varying(191),
    wiki_data_id character varying(255)
)
INHERITS (common.created_at, common.updated_at, common.immutable, common.forbid_insert);


ALTER TABLE common.country OWNER TO postgres;

--
-- Name: TABLE country; Type: COMMENT; Schema: common; Owner: postgres
--

COMMENT ON TABLE common.country IS 'iso 3166';


--
-- Name: COLUMN country.wiki_data_id; Type: COMMENT; Schema: common; Owner: postgres
--

COMMENT ON COLUMN common.country.wiki_data_id IS 'Rapid API GeoDB Cities';


--
-- Name: country_id_seq; Type: SEQUENCE; Schema: common; Owner: postgres
--

CREATE SEQUENCE common.country_id_seq
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER TABLE common.country_id_seq OWNER TO postgres;

--
-- Name: country_id_seq; Type: SEQUENCE OWNED BY; Schema: common; Owner: postgres
--

ALTER SEQUENCE common.country_id_seq OWNED BY common.country.id;


--
-- Name: currency; Type: TABLE; Schema: common; Owner: postgres
--

CREATE TABLE common.currency (
    id integer NOT NULL,
    code character(3) NOT NULL,
    numeric_code character(3) NOT NULL,
    minor_unit smallint,
    name character varying(59) NOT NULL,
    countries character varying(652) NOT NULL
)
INHERITS (common.created_at, common.immutable, common.forbid_insert);


ALTER TABLE common.currency OWNER TO postgres;

--
-- Name: TABLE currency; Type: COMMENT; Schema: common; Owner: postgres
--

COMMENT ON TABLE common.currency IS 'iso 4217';


--
-- Name: currency_id_seq; Type: SEQUENCE; Schema: common; Owner: postgres
--

CREATE SEQUENCE common.currency_id_seq
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER TABLE common.currency_id_seq OWNER TO postgres;

--
-- Name: currency_id_seq; Type: SEQUENCE OWNED BY; Schema: common; Owner: postgres
--

ALTER SEQUENCE common.currency_id_seq OWNED BY common.currency.id;


--
-- Name: currency_price; Type: TABLE; Schema: common; Owner: postgres
--

CREATE TABLE common.currency_price (
    id integer NOT NULL,
    "from" integer NOT NULL,
    "to" jsonb NOT NULL,
    CONSTRAINT currency_price_to_check CHECK ((jsonb_typeof("to") = 'object'::text)),
    CONSTRAINT currency_price_to_check1 CHECK ((("to" ? 'USD'::text) AND ("to" ? 'EUR'::text) AND ("to" ? 'UAH'::text) AND ("to" ? 'KZT'::text))),
    CONSTRAINT currency_price_to_check2 CHECK (((jsonb_typeof(("to" -> 'USD'::text)) = 'number'::text) AND (jsonb_typeof(("to" -> 'EUR'::text)) = 'number'::text) AND (jsonb_typeof(("to" -> 'UAH'::text)) = 'number'::text) AND (jsonb_typeof(("to" -> 'KZT'::text)) = 'number'::text)))
)
INHERITS (common.created_at, common.updated_at);


ALTER TABLE common.currency_price OWNER TO postgres;

--
-- Name: currency_price_id_seq; Type: SEQUENCE; Schema: common; Owner: postgres
--

CREATE SEQUENCE common.currency_price_id_seq
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER TABLE common.currency_price_id_seq OWNER TO postgres;

--
-- Name: currency_price_id_seq; Type: SEQUENCE OWNED BY; Schema: common; Owner: postgres
--

ALTER SEQUENCE common.currency_price_id_seq OWNED BY common.currency_price.id;


--
-- Name: discount_tier; Type: TABLE; Schema: common; Owner: postgres
--

CREATE TABLE common.discount_tier (
    min_amount integer NOT NULL,
    discount double precision
)
INHERITS (common.created_at);


ALTER TABLE common.discount_tier OWNER TO postgres;

--
-- Name: language; Type: TABLE; Schema: common; Owner: postgres
--

CREATE TABLE common.language (
    id integer NOT NULL,
    name character varying(51) NOT NULL,
    native_name character varying(57) NOT NULL,
    family character varying(19) NOT NULL,
    code character(2) NOT NULL,
    code_2 character(3) NOT NULL,
    code_2_b character(3)
)
INHERITS (common.created_at, common.immutable, common.forbid_insert);


ALTER TABLE common.language OWNER TO postgres;

--
-- Name: TABLE language; Type: COMMENT; Schema: common; Owner: postgres
--

COMMENT ON TABLE common.language IS 'iso 639';


--
-- Name: language_id_seq; Type: SEQUENCE; Schema: common; Owner: postgres
--

CREATE SEQUENCE common.language_id_seq
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


ALTER TABLE common.language_id_seq OWNER TO postgres;

--
-- Name: language_id_seq; Type: SEQUENCE OWNED BY; Schema: common; Owner: postgres
--

ALTER SEQUENCE common.language_id_seq OWNED BY common.language.id;


--
-- Name: ordered_order_seq; Type: SEQUENCE; Schema: common; Owner: postgres
--

ALTER TABLE common.ordered ALTER COLUMN "order" ADD GENERATED BY DEFAULT AS IDENTITY (
    SEQUENCE NAME common.ordered_order_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1
);


--
-- Name: country created_at; Type: DEFAULT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.country ALTER COLUMN created_at SET DEFAULT now();


--
-- Name: country updated_at; Type: DEFAULT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.country ALTER COLUMN updated_at SET DEFAULT now();


--
-- Name: country id; Type: DEFAULT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.country ALTER COLUMN id SET DEFAULT nextval('common.country_id_seq'::regclass);


--
-- Name: currency created_at; Type: DEFAULT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.currency ALTER COLUMN created_at SET DEFAULT now();


--
-- Name: currency id; Type: DEFAULT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.currency ALTER COLUMN id SET DEFAULT nextval('common.currency_id_seq'::regclass);


--
-- Name: currency_price created_at; Type: DEFAULT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.currency_price ALTER COLUMN created_at SET DEFAULT now();


--
-- Name: currency_price updated_at; Type: DEFAULT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.currency_price ALTER COLUMN updated_at SET DEFAULT now();


--
-- Name: currency_price id; Type: DEFAULT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.currency_price ALTER COLUMN id SET DEFAULT nextval('common.currency_price_id_seq'::regclass);


--
-- Name: discount_tier created_at; Type: DEFAULT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.discount_tier ALTER COLUMN created_at SET DEFAULT now();


--
-- Name: language created_at; Type: DEFAULT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.language ALTER COLUMN created_at SET DEFAULT now();


--
-- Name: language id; Type: DEFAULT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.language ALTER COLUMN id SET DEFAULT nextval('common.language_id_seq'::regclass);


--
-- Name: country country_pkey; Type: CONSTRAINT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.country
    ADD CONSTRAINT country_pkey PRIMARY KEY (id);


--
-- Name: currency currency_code_unique; Type: CONSTRAINT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.currency
    ADD CONSTRAINT currency_code_unique UNIQUE (code);


--
-- Name: currency currency_pkey; Type: CONSTRAINT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.currency
    ADD CONSTRAINT currency_pkey PRIMARY KEY (id);


--
-- Name: currency_price currency_price_pkey; Type: CONSTRAINT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.currency_price
    ADD CONSTRAINT currency_price_pkey PRIMARY KEY (id);


--
-- Name: discount_tier discount_tier_pkey; Type: CONSTRAINT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.discount_tier
    ADD CONSTRAINT discount_tier_pkey PRIMARY KEY (min_amount);


--
-- Name: language language_pkey; Type: CONSTRAINT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.language
    ADD CONSTRAINT language_pkey PRIMARY KEY (id);


--
-- Name: currency_price currency_price_from_fkey; Type: FK CONSTRAINT; Schema: common; Owner: postgres
--

ALTER TABLE ONLY common.currency_price
    ADD CONSTRAINT currency_price_from_fkey FOREIGN KEY ("from") REFERENCES common.currency(id);


--
-- Name: SCHEMA common; Type: ACL; Schema: -; Owner: postgres
--

GRANT USAGE ON SCHEMA common TO vproxy;
GRANT USAGE ON SCHEMA common TO vgate_admin;
GRANT USAGE ON SCHEMA common TO vgate_provider;
GRANT USAGE ON SCHEMA common TO anon;


--
-- Name: FUNCTION is_valid_domain(input text); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.is_valid_domain(input text) TO vproxy;
GRANT ALL ON FUNCTION common.is_valid_domain(input text) TO vgate_admin;
GRANT ALL ON FUNCTION common.is_valid_domain(input text) TO vgate_provider;


--
-- Name: FUNCTION is_valid_ip(input text); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.is_valid_ip(input text) TO vproxy;
GRANT ALL ON FUNCTION common.is_valid_ip(input text) TO vgate_admin;
GRANT ALL ON FUNCTION common.is_valid_ip(input text) TO vgate_provider;


--
-- Name: FUNCTION is_valid_port(input integer); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.is_valid_port(input integer) TO vproxy;
GRANT ALL ON FUNCTION common.is_valid_port(input integer) TO vgate_admin;
GRANT ALL ON FUNCTION common.is_valid_port(input integer) TO vgate_provider;


--
-- Name: FUNCTION add_prevent_created_at_trigger_to_new_immutable_child(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.add_prevent_created_at_trigger_to_new_immutable_child() TO vproxy;
GRANT ALL ON FUNCTION common.add_prevent_created_at_trigger_to_new_immutable_child() TO vgate_admin;
GRANT ALL ON FUNCTION common.add_prevent_created_at_trigger_to_new_immutable_child() TO vgate_provider;


--
-- Name: FUNCTION add_trigger_to_new_child(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.add_trigger_to_new_child() TO vproxy;
GRANT ALL ON FUNCTION common.add_trigger_to_new_child() TO vgate_admin;
GRANT ALL ON FUNCTION common.add_trigger_to_new_child() TO vgate_provider;


--
-- Name: FUNCTION cannot_create_table_that_not_inherits_from_created_at(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.cannot_create_table_that_not_inherits_from_created_at() TO vproxy;
GRANT ALL ON FUNCTION common.cannot_create_table_that_not_inherits_from_created_at() TO vgate_admin;
GRANT ALL ON FUNCTION common.cannot_create_table_that_not_inherits_from_created_at() TO vgate_provider;


--
-- Name: FUNCTION fetch_currency(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.fetch_currency() TO vproxy;
GRANT ALL ON FUNCTION common.fetch_currency() TO vgate_admin;
GRANT ALL ON FUNCTION common.fetch_currency() TO vgate_provider;


--
-- Name: FUNCTION format_traffic(bytes bigint); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.format_traffic(bytes bigint) TO vproxy;
GRANT ALL ON FUNCTION common.format_traffic(bytes bigint) TO vgate_admin;
GRANT ALL ON FUNCTION common.format_traffic(bytes bigint) TO vgate_provider;


--
-- Name: FUNCTION gateway_admin_role(gateway_schema text); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.gateway_admin_role(gateway_schema text) TO vproxy;
GRANT ALL ON FUNCTION common.gateway_admin_role(gateway_schema text) TO vgate_admin;
GRANT ALL ON FUNCTION common.gateway_admin_role(gateway_schema text) TO vgate_provider;


--
-- Name: FUNCTION gateway_provider_role(gateway_schema text); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.gateway_provider_role(gateway_schema text) TO vproxy;
GRANT ALL ON FUNCTION common.gateway_provider_role(gateway_schema text) TO vgate_admin;
GRANT ALL ON FUNCTION common.gateway_provider_role(gateway_schema text) TO vgate_provider;


--
-- Name: FUNCTION gateway_user_role(gateway_schema text); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.gateway_user_role(gateway_schema text) TO vproxy;
GRANT ALL ON FUNCTION common.gateway_user_role(gateway_schema text) TO vgate_admin;
GRANT ALL ON FUNCTION common.gateway_user_role(gateway_schema text) TO vgate_provider;


--
-- Name: FUNCTION generate_scram_hash(password text); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.generate_scram_hash(password text) TO vproxy;
GRANT ALL ON FUNCTION common.generate_scram_hash(password text) TO vgate_admin;
GRANT ALL ON FUNCTION common.generate_scram_hash(password text) TO vgate_provider;


--
-- Name: FUNCTION get_gateway_schema(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.get_gateway_schema() TO vproxy;
GRANT ALL ON FUNCTION common.get_gateway_schema() TO vgate_admin;
GRANT ALL ON FUNCTION common.get_gateway_schema() TO vgate_provider;


--
-- Name: FUNCTION get_header(item text); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.get_header(item text) TO vproxy;
GRANT ALL ON FUNCTION common.get_header(item text) TO vgate_admin;
GRANT ALL ON FUNCTION common.get_header(item text) TO vgate_provider;


--
-- Name: FUNCTION get_headers(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.get_headers() TO vproxy;
GRANT ALL ON FUNCTION common.get_headers() TO vgate_admin;
GRANT ALL ON FUNCTION common.get_headers() TO vgate_provider;


--
-- Name: FUNCTION get_inner_schema(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.get_inner_schema() TO vproxy;
GRANT ALL ON FUNCTION common.get_inner_schema() TO vgate_admin;
GRANT ALL ON FUNCTION common.get_inner_schema() TO vgate_provider;


--
-- Name: FUNCTION get_jwt_secret(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.get_jwt_secret() TO vproxy;
GRANT ALL ON FUNCTION common.get_jwt_secret() TO vgate_admin;
GRANT ALL ON FUNCTION common.get_jwt_secret() TO vgate_provider;


--
-- Name: FUNCTION header_required(name text); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.header_required(name text) TO vproxy;
GRANT ALL ON FUNCTION common.header_required(name text) TO vgate_admin;
GRANT ALL ON FUNCTION common.header_required(name text) TO vgate_provider;


--
-- Name: FUNCTION prevent_created_at_update(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.prevent_created_at_update() TO vproxy;
GRANT ALL ON FUNCTION common.prevent_created_at_update() TO vgate_admin;
GRANT ALL ON FUNCTION common.prevent_created_at_update() TO vgate_provider;


--
-- Name: FUNCTION prevent_modifications(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.prevent_modifications() TO vproxy;
GRANT ALL ON FUNCTION common.prevent_modifications() TO vgate_admin;
GRANT ALL ON FUNCTION common.prevent_modifications() TO vgate_provider;


--
-- Name: FUNCTION secure_hash_equals(hash1 bytea, hash2 bytea); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.secure_hash_equals(hash1 bytea, hash2 bytea) TO vproxy;
GRANT ALL ON FUNCTION common.secure_hash_equals(hash1 bytea, hash2 bytea) TO vgate_admin;
GRANT ALL ON FUNCTION common.secure_hash_equals(hash1 bytea, hash2 bytea) TO vgate_provider;


--
-- Name: FUNCTION update_forbid_insert_to_prevent_modifications(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.update_forbid_insert_to_prevent_modifications() TO vproxy;
GRANT ALL ON FUNCTION common.update_forbid_insert_to_prevent_modifications() TO vgate_admin;
GRANT ALL ON FUNCTION common.update_forbid_insert_to_prevent_modifications() TO vgate_provider;


--
-- Name: FUNCTION update_immutable_to_prevent_modifications(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.update_immutable_to_prevent_modifications() TO vproxy;
GRANT ALL ON FUNCTION common.update_immutable_to_prevent_modifications() TO vgate_admin;
GRANT ALL ON FUNCTION common.update_immutable_to_prevent_modifications() TO vgate_provider;


--
-- Name: FUNCTION update_updated_at_column(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.update_updated_at_column() TO vproxy;
GRANT ALL ON FUNCTION common.update_updated_at_column() TO vgate_admin;
GRANT ALL ON FUNCTION common.update_updated_at_column() TO vgate_provider;


--
-- Name: FUNCTION validate_currency_price_to_json(); Type: ACL; Schema: common; Owner: postgres
--

GRANT ALL ON FUNCTION common.validate_currency_price_to_json() TO vproxy;
GRANT ALL ON FUNCTION common.validate_currency_price_to_json() TO vgate_admin;
GRANT ALL ON FUNCTION common.validate_currency_price_to_json() TO vgate_provider;


--
-- Name: TABLE created_at; Type: ACL; Schema: common; Owner: postgres
--

GRANT SELECT ON TABLE common.created_at TO vproxy;
GRANT SELECT ON TABLE common.created_at TO vgate_admin;
GRANT SELECT ON TABLE common.created_at TO vgate_provider;


--
-- Name: TABLE updated_at; Type: ACL; Schema: common; Owner: postgres
--

GRANT SELECT ON TABLE common.updated_at TO vproxy;
GRANT SELECT ON TABLE common.updated_at TO vgate_admin;
GRANT SELECT ON TABLE common.updated_at TO vgate_provider;


--
-- Name: TABLE forbid_insert; Type: ACL; Schema: common; Owner: postgres
--

GRANT SELECT ON TABLE common.forbid_insert TO vproxy;
GRANT SELECT ON TABLE common.forbid_insert TO vgate_admin;
GRANT SELECT ON TABLE common.forbid_insert TO vgate_provider;


--
-- Name: TABLE immutable; Type: ACL; Schema: common; Owner: postgres
--

GRANT SELECT ON TABLE common.immutable TO vproxy;
GRANT SELECT ON TABLE common.immutable TO vgate_admin;
GRANT SELECT ON TABLE common.immutable TO vgate_provider;


--
-- Name: TABLE country; Type: ACL; Schema: common; Owner: postgres
--

GRANT SELECT ON TABLE common.country TO vproxy;
GRANT SELECT ON TABLE common.country TO vgate_admin;
GRANT SELECT ON TABLE common.country TO vgate_provider;


--
-- Name: TABLE currency; Type: ACL; Schema: common; Owner: postgres
--

GRANT SELECT ON TABLE common.currency TO vproxy;
GRANT SELECT ON TABLE common.currency TO vgate_admin;
GRANT SELECT ON TABLE common.currency TO vgate_provider;


--
-- Name: TABLE currency_price; Type: ACL; Schema: common; Owner: postgres
--

GRANT SELECT ON TABLE common.currency_price TO vproxy;
GRANT SELECT ON TABLE common.currency_price TO vgate_admin;
GRANT SELECT ON TABLE common.currency_price TO vgate_provider;


--
-- Name: TABLE language; Type: ACL; Schema: common; Owner: postgres
--

GRANT SELECT ON TABLE common.language TO vproxy;
GRANT SELECT ON TABLE common.language TO vgate_admin;
GRANT SELECT ON TABLE common.language TO vgate_provider;


--
-- PostgreSQL database dump complete
--

