Загрузка данных
CREATE OR REPLACE FUNCTION integration.submit_unv_application_api(p_request text)
RETURNS text
LANGUAGE plpgsql
AS $function$
DECLARE
v_request jsonb;
v_request_id uuid;
v_client_id integer;
v_contract_id integer;
v_flag integer;
v_year integer;
v_current_year integer :=
EXTRACT(YEAR FROM CURRENT_DATE)::integer;
v_max_year integer;
v_contract jsonb;
v_period jsonb;
v_contracts jsonb := '[]'::jsonb;
v_periods jsonb;
v_payload jsonb;
v_saved report.unv_api_request%ROWTYPE;
v_available jsonb;
v_available_contract jsonb;
v_available_period jsonb;
v_selected jsonb := '[]'::jsonb;
v_selected_item jsonb;
v_items jsonb := '[]'::jsonb;
v_response jsonb;
v_snils text;
v_code_id report.smev_register_codes.id%TYPE;
v_registry_id report.smev_register.id%TYPE;
v_item_id report.unv_registry_item.id%TYPE;
v_amount numeric;
v_deduction numeric;
v_rows integer;
BEGIN
-- Проверяем формат запроса.
BEGIN
v_request := p_request::jsonb;
EXCEPTION
WHEN invalid_text_representation THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Передан невалидный JSON';
END;
IF jsonb_typeof(v_request) IS DISTINCT FROM 'object' THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Ожидается JSON-объект';
END IF;
BEGIN
v_request_id := (v_request ->> 'request_id')::uuid;
EXCEPTION
WHEN invalid_text_representation THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'request_id должен быть UUID';
END;
IF v_request_id IS NULL THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Не указан request_id';
END IF;
IF jsonb_typeof(v_request -> 'id') IS DISTINCT FROM 'number'
OR COALESCE(v_request ->> 'id', '') !~ '^[1-9][0-9]{0,9}$'
THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'id должен быть положительным целым числом';
END IF;
IF (v_request ->> 'id')::bigint > 2147483647 THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'id превышает допустимый диапазон';
END IF;
v_client_id := (v_request ->> 'id')::integer;
IF jsonb_typeof(v_request -> 'contracts')
IS DISTINCT FROM 'array'
THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'contracts должен быть массивом';
END IF;
IF jsonb_array_length(v_request -> 'contracts') = 0 THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Не выбраны договоры';
END IF;
-- Проверяем договоры и приводим запрос к единому виду.
FOR v_contract IN
SELECT value
FROM jsonb_array_elements(v_request -> 'contracts')
LOOP
IF jsonb_typeof(v_contract) IS DISTINCT FROM 'object' THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Элемент contracts должен быть объектом';
END IF;
IF jsonb_typeof(v_contract -> 'contract_id')
IS DISTINCT FROM 'number'
OR COALESCE(v_contract ->> 'contract_id', '')
!~ '^[1-9][0-9]{0,9}$'
THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Некорректный contract_id';
END IF;
IF (v_contract ->> 'contract_id')::bigint > 2147483647 THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'contract_id превышает допустимый диапазон';
END IF;
v_contract_id := (v_contract ->> 'contract_id')::integer;
IF EXISTS (
SELECT 1
FROM jsonb_array_elements(v_contracts) x
WHERE (x.value ->> 'contract_id')::integer =
v_contract_id
) THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Один договор указан несколько раз';
END IF;
IF jsonb_typeof(v_contract -> 'annual_deduction_flag')
IS DISTINCT FROM 'number'
OR COALESCE(v_contract ->> 'annual_deduction_flag', '')
NOT IN ('0', '1')
THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'annual_deduction_flag должен быть 0 или 1';
END IF;
v_flag := (v_contract ->> 'annual_deduction_flag')::integer;
IF jsonb_typeof(v_contract -> 'periods')
IS DISTINCT FROM 'array'
THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'periods должен быть массивом годов';
END IF;
IF jsonb_array_length(v_contract -> 'periods')
NOT BETWEEN 1 AND 3
THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Для договора нужно выбрать от одного до трёх периодов';
END IF;
v_periods := '[]'::jsonb;
FOR v_period IN
SELECT value
FROM jsonb_array_elements(v_contract -> 'periods')
LOOP
IF jsonb_typeof(v_period) IS DISTINCT FROM 'number'
OR v_period::text !~ '^[0-9]{4}$'
THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Налоговый период должен быть четырёхзначным целым годом';
END IF;
v_year := v_period::text::integer;
IF v_periods @> jsonb_build_array(v_year) THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Для договора повторяется налоговый период';
END IF;
v_periods :=
v_periods || jsonb_build_array(v_year);
END LOOP;
SELECT jsonb_agg(
x.value ORDER BY x.value::text::integer
)
INTO v_periods
FROM jsonb_array_elements(v_periods) x;
v_contracts :=
v_contracts || jsonb_build_array(
jsonb_build_object(
'contract_id', v_contract_id,
'periods', v_periods,
'annual_deduction_flag', v_flag
)
);
END LOOP;
SELECT jsonb_agg(
x.value ORDER BY (x.value ->> 'contract_id')::integer
)
INTO v_contracts
FROM jsonb_array_elements(v_contracts) x;
v_payload := jsonb_build_object(
'id', v_client_id,
'contracts', v_contracts
);
-- Запоминаем запрос.
-- Порядок договоров/годов и служебные RequestParams не влияют на распознавание повторной подачи.
INSERT INTO report.unv_api_request (
request_id,
client_id,
request_payload
)
VALUES (
v_request_id,
v_client_id,
v_payload
)
ON CONFLICT (request_id) DO NOTHING;
GET DIAGNOSTICS v_rows = ROW_COUNT;
IF v_rows = 0 THEN
SELECT *
INTO STRICT v_saved
FROM report.unv_api_request
WHERE request_id = v_request_id
FOR UPDATE;
IF v_saved.client_id <> v_client_id
OR v_saved.request_payload IS DISTINCT FROM v_payload
THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'request_id уже использован с другими параметрами';
END IF;
IF v_saved.completed_at IS NULL
OR v_saved.response_payload IS NULL
THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'У запроса отсутствует завершённый результат';
END IF;
RETURN v_saved.response_payload::text;
END IF;
-- Последовательная обработка новых запросов этого POST.
-- Ручная операция и регламент пока не используют этот lock.
PERFORM pg_advisory_xact_lock(20260916, 1);
SELECT NULLIF(TRIM(i.insurance_number), '')
INTO v_snils
FROM ourpension.individual i
WHERE i.id = v_client_id;
IF v_snils IS NULL THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Клиент не найден или не указан СНИЛС';
END IF;
-- Используем существующий GET для проверки доступности и получения рассчитанных сумм.
v_available :=
integration.get_unv_available_contracts_api(
jsonb_build_object('id', v_client_id)::text
)::jsonb;
IF jsonb_typeof(v_available -> 'contracts')
IS DISTINCT FROM 'array'
THEN
RAISE EXCEPTION USING
ERRCODE = 'XX000',
MESSAGE = 'GET доступных договоров вернул неожиданный формат';
END IF;
-- Проверяем все выбранные пары до создания реестров.
FOR v_contract IN
SELECT value
FROM jsonb_array_elements(v_contracts)
LOOP
v_contract_id :=
(v_contract ->> 'contract_id')::integer;
SELECT x.value
INTO v_available_contract
FROM jsonb_array_elements(v_available -> 'contracts') x
WHERE (x.value ->> 'contract_id')::integer =
v_contract_id;
IF v_available_contract IS NULL THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = format(
'Договор %s недоступен для УНВ данного клиента',
v_contract_id
);
END IF;
IF jsonb_typeof(v_available_contract -> 'periods')
IS DISTINCT FROM 'array'
OR COALESCE(
v_available_contract ->> 'contract_type',
''
) NOT IN ('NPO', 'PDS')
THEN
RAISE EXCEPTION USING
ERRCODE = 'XX000',
MESSAGE = format(
'GET вернул некорректные данные договора %s',
v_contract_id
);
END IF;
FOR v_period IN
SELECT value
FROM jsonb_array_elements(v_contract -> 'periods')
LOOP
v_year := v_period::text::integer;
IF v_year < v_current_year - 3
OR v_year >= v_current_year
THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = 'Доступны только три завершённых налоговых года';
END IF;
SELECT x.value
INTO v_available_period
FROM jsonb_array_elements(
v_available_contract -> 'periods'
) x
WHERE (x.value ->> 'tax_period_year')::integer =
v_year;
IF v_available_period IS NULL THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = format(
'Период %s недоступен по договору %s',
v_year,
v_contract_id
);
END IF;
v_amount :=
(v_available_period ->> 'contributions_amount')::numeric;
v_deduction :=
(v_available_period ->> 'preliminary_deduction_amount')::numeric;
IF v_amount IS NULL
OR v_amount <= 0
OR v_deduction IS NULL
OR v_deduction < 0
THEN
RAISE EXCEPTION USING
ERRCODE = 'XX000',
MESSAGE = 'GET вернул некорректные суммы';
END IF;
-- Повтор в любом неудалённом реестре отклоняется.
-- Корректировки и отказы требуют отдельного сценария.
IF EXISTS (
SELECT 1
FROM report.unv_registry_item i
JOIN report.smev_register r
ON r.id = i.registry_id
WHERE i.client_id = v_client_id
AND i.contract_id = v_contract_id
AND i.tax_period_year = v_year
AND r.sample_status IS DISTINCT FROM 'Удалён'
) THEN
RAISE EXCEPTION USING
ERRCODE = 'P0001',
MESSAGE = format(
'Договор %s за %s год уже включён в неудалённый реестр',
v_contract_id,
v_year
);
END IF;
v_selected :=
v_selected || jsonb_build_array(
jsonb_build_object(
'contract_id', v_contract_id,
'tax_period_year', v_year,
'contract_type',
v_available_contract ->> 'contract_type',
'annual_deduction_flag',
(v_contract ->> 'annual_deduction_flag')::integer,
'contributions_amount', v_amount,
'preliminary_deduction_amount', v_deduction
)
);
END LOOP;
END LOOP;
SELECT id
INTO v_code_id
FROM report.smev_register_codes
WHERE short_name = 'УНВ'
ORDER BY id
LIMIT 1;
IF v_code_id IS NULL THEN
RAISE EXCEPTION USING
ERRCODE = 'XX000',
MESSAGE = 'В справочнике smev_register_codes отсутствует УНВ';
END IF;
-- Создаём строки выбранных договоров и периодов.
FOR v_selected_item IN
SELECT value
FROM jsonb_array_elements(v_selected)
ORDER BY
(value ->> 'tax_period_year')::integer,
(value ->> 'contract_id')::integer
LOOP
v_year :=
(v_selected_item ->> 'tax_period_year')::integer;
-- Дополняем только реестр в статусе «Сформирован».
SELECT r.id
INTO v_registry_id
FROM report.smev_register r
WHERE r.smev_register_type_code = 'УНВ'
AND r."year" = v_year
AND r."type" = 'OUT'
AND r.sample_status = 'Сформирован'
ORDER BY r.id DESC
LIMIT 1
FOR UPDATE;
IF v_registry_id IS NULL THEN
INSERT INTO report.smev_register (
id,
smev_register_type_code,
smev_register_codes_id,
"year",
"date",
"type",
sample_status
)
VALUES (
nextval('report.smev_register_id_sequence'),
'УНВ',
v_code_id,
v_year,
CURRENT_DATE,
'OUT',
'Сформирован'
)
RETURNING id INTO v_registry_id;
END IF;
INSERT INTO report.unv_registry_item (
registry_id,
client_id,
snils,
contract_id,
contract_type,
tax_period_year,
contributions_amount,
preliminary_deduction_amount,
annual_deduction_flag,
status,
send_count
)
VALUES (
v_registry_id,
v_client_id,
v_snils,
(v_selected_item ->> 'contract_id')::integer,
v_selected_item ->> 'contract_type',
v_year,
(v_selected_item ->> 'contributions_amount')::numeric,
(v_selected_item ->> 'preliminary_deduction_amount')::numeric,
(v_selected_item ->> 'annual_deduction_flag')::integer,
'Сформирован',
0
)
RETURNING id INTO v_item_id;
v_items :=
v_items || jsonb_build_array(
jsonb_build_object(
'item_id', v_item_id,
'registry_id', v_registry_id,
'contract_id',
(v_selected_item ->> 'contract_id')::integer,
'tax_period_year', v_year,
'status', 'Сформирован'
)
);
END LOOP;
-- Сохраняем настройку ежегодного вычета по каждому договору.
FOR v_contract IN
SELECT value
FROM jsonb_array_elements(v_contracts)
LOOP
SELECT MAX(x.value::text::integer)
INTO v_max_year
FROM jsonb_array_elements(v_contract -> 'periods') x;
INSERT INTO report.unv_annual_setting AS s (
client_id,
contract_id,
annual_deduction_flag,
selected_tax_period,
source
)
VALUES (
v_client_id,
(v_contract ->> 'contract_id')::integer,
(v_contract ->> 'annual_deduction_flag')::integer,
v_max_year,
'LKK'
)
ON CONFLICT (client_id, contract_id)
DO UPDATE SET
annual_deduction_flag =
EXCLUDED.annual_deduction_flag,
-- Подача за старый год не сдвигает
-- начало ежегодного включения назад.
selected_tax_period = GREATEST(
s.selected_tax_period,
EXCLUDED.selected_tax_period
),
source = EXCLUDED.source,
updated_at = CURRENT_TIMESTAMP;
END LOOP;
v_response := jsonb_build_object(
'status', 'success',
'request_id', v_request_id,
'client_id', v_client_id,
'message', 'Заявление принято. Строки реестра сформированы.',
'items', v_items
);
UPDATE report.unv_api_request
SET
response_payload = v_response,
completed_at = CURRENT_TIMESTAMP
WHERE request_id = v_request_id;
RETURN v_response::text;
EXCEPTION
WHEN SQLSTATE 'P0001' THEN
-- Все изменения текущего вызова откатываются.
-- Остальные SQL-ошибки передаются в ws.sp_request_root.
RETURN jsonb_build_object(
'status', 'error',
'request_id', v_request_id,
'message', SQLERRM
)::text;
END;
$function$