Загрузка данных
-- DROP FUNCTION ourpension.f_process_snv_file(int4, text, int4);
CREATE OR REPLACE FUNCTION ourpension.f_process_snv_file(p_file_id integer, p_username text, p_user_id integer)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
v_source jsonb;
v_mapped jsonb;
v_ready jsonb;
v_errors jsonb := '[]'::jsonb;
v_result jsonb;
v_saved jsonb;
v_row record;
v_bad record;
v_row_errors text[];
v_source_count integer;
v_count integer;
v_batch_id integer;
v_registered_count integer := 0;
v_journal_updated_count integer;
v_version_datetime timestamp without time zone;
BEGIN
IF p_file_id IS NULL OR p_file_id <= 0 THEN
RAISE EXCEPTION 'СНВ: требуется положительный fileId';
END IF;
IF p_username IS NULL OR btrim(p_username) = ''
OR p_user_id IS NULL OR p_user_id <= 0 THEN
RAISE EXCEPTION 'СНВ: требуются username и положительный user_id регламента';
END IF;
-- Два одновременных вызова для одного файла выполняются последовательно.
PERFORM pg_advisory_xact_lock(hashtext('ourpension.snv_auto.file'), p_file_id);
SELECT h.result INTO v_saved
FROM ourpension.snv_auto_file_processing h
WHERE h.file_id = p_file_id;
IF FOUND THEN
RETURN v_saved || jsonb_build_object('already_processed', true);
END IF;
-- Не позволяем функции создания справки забрать чужое ручное вложение.
PERFORM pg_advisory_xact_lock(
hashtext('ourpension.snv_auto.username'), hashtext(p_username)
);
IF EXISTS (
SELECT 1 FROM dev.upload_file f
WHERE f.code = 'snv_card' AND f.username = p_username
) THEN
RAISE EXCEPTION 'СНВ: у пользователя запуска есть ручное вложение snv_card; fileId=%', p_file_id;
END IF;
-- Снимок уже загруженных строк. Во всех дальнейших шагах используется
-- этот же набор, а не пользовательский выбор tools.selection.
PERFORM l.id FROM ourpension.load_snv l
WHERE l.file_id = p_file_id ORDER BY l.id FOR UPDATE;
SELECT jsonb_agg(to_jsonb(l) ORDER BY l.id) INTO v_source
FROM ourpension.load_snv l WHERE l.file_id = p_file_id;
IF v_source IS NULL THEN
RAISE EXCEPTION 'СНВ: файл % не содержит загруженных строк', p_file_id;
END IF;
v_source_count := jsonb_array_length(v_source);
-- Проверки исходного валидатора без зависимости от tools.selection.
FOR v_row IN
SELECT l.* FROM jsonb_populate_recordset(NULL::ourpension.load_snv, v_source) l
LOOP
v_row_errors := ARRAY[]::text[];
IF NOT EXISTS (
SELECT 1 FROM ourpension.contract c
WHERE c.number = v_row.contract_number
) THEN
v_row_errors := array_append(v_row_errors, 'Не найден номер договора');
END IF;
IF NOT EXISTS (
SELECT 1 FROM ourpension.contract c
WHERE c.number = v_row.contract_number AND c.date = v_row.contract_date
) THEN
v_row_errors := array_append(v_row_errors, 'Не совпадает дата договора');
END IF;
IF EXISTS (
SELECT 1 FROM ourpension.contract c
JOIN ourpension.individual i ON i.id = c.individual_id
WHERE c.number = v_row.contract_number
AND i.inn = v_row.person_inn
AND c.service_type <> v_row.service_type
) THEN
v_row_errors := array_append(v_row_errors, 'Не совпадает вид деятельности');
END IF;
IF NOT EXISTS (
SELECT 1 FROM ourpension.contract c
JOIN ourpension.individual i ON i.id = c.individual_id
WHERE c.number = v_row.contract_number
AND i.first_name = v_row.person_first_name
) THEN
v_row_errors := array_append(v_row_errors, 'Не совпадает имя клиента');
END IF;
IF NOT EXISTS (
SELECT 1 FROM ourpension.contract c
JOIN ourpension.individual i ON i.id = c.individual_id
WHERE c.number = v_row.contract_number
AND i.middle_name = v_row.person_middle_name
) THEN
v_row_errors := array_append(v_row_errors, 'Не совпадает отчество клиента');
END IF;
-- Исключаем тихий пропуск создания и ошибки преобразования года.
IF v_row.deduction_sign IS NULL
OR v_row.deduction_sign::text NOT IN ('RECEIVED', 'NOT_RECEIVED') THEN
v_row_errors := array_append(v_row_errors, 'Не определён признак получения вычета');
END IF;
IF v_row.service_type IS NULL
OR v_row.service_type::text NOT IN ('NPO', 'PDS') THEN
v_row_errors := array_append(v_row_errors, 'Не определён вид деятельности НПО/ПДС');
END IF;
IF v_row.year IS NULL OR btrim(v_row.year::text) !~ '^[0-9]{1,4}$' THEN
v_row_errors := array_append(v_row_errors, 'Некорректный год справки');
ELSIF v_row.year::integer < 1 THEN
v_row_errors := array_append(v_row_errors, 'Некорректный год справки');
END IF;
-- Без отправленного запроса проставить обязательную отметку невозможно.
IF NOT EXISTS (
SELECT 1
FROM report.fns_reference_request_journal fns
JOIN dev.model_file_to_verification mfc
ON mfc.id = fns.model_file_to_verification_id
JOIN report.smev_register sr ON sr.id = mfc.register_id
WHERE fns.contract_number = v_row.contract_number
AND sr.number = v_row.request_number::text
AND fns.is_sent_to_fns = true
) THEN
v_row_errors := array_append(v_row_errors, 'Не найден отправленный запрос в журнале ФНС');
END IF;
IF cardinality(v_row_errors) > 0 THEN
v_errors := v_errors || jsonb_build_array(jsonb_build_object(
'load_snv_id', v_row.id, 'errors', to_jsonb(v_row_errors)
));
END IF;
END LOOP;
IF jsonb_array_length(v_errors) > 0 THEN
RAISE EXCEPTION USING
MESSAGE = format('СНВ: проверка файла %s завершилась ошибками; регистрация не начата', p_file_id),
DETAIL = v_errors::text;
END IF;
-- Сопоставление из модели. Номер, дата и вид деятельности должны
-- относиться к одному договору; иначе строка не готова к регистрации.
WITH base AS (
SELECT l.id AS load_snv_id,
ct.id AS application_termination_contract_id,
c.individual_id, l.sum_tax_deduction,
l.year::integer AS year, l.deduction_sign::text AS type,
c.service_type::text AS service_type,
sh.sharer_id_1c::text AS sharer_id_1c
FROM jsonb_populate_recordset(NULL::ourpension.load_snv, v_source) l
JOIN ourpension.contract c
ON c.number = l.contract_number AND c.date = l.contract_date
AND c.service_type = l.service_type
JOIN ourpension.individual i ON i.id = c.individual_id
AND lower(btrim(i.last_name)) = lower(btrim(l.person_last_name))
AND lower(btrim(i.first_name)) = lower(btrim(l.person_first_name))
JOIN ourpension.sharer sh ON sh.contract_id = c.id
LEFT JOIN ourpension.application_termination_contract_data ctd
ON ctd.sharer_id = sh.id
JOIN ourpension.application_termination_contract ct
ON CASE WHEN ctd.sharer_id IS NULL THEN ct.sharer_id = sh.id
ELSE ct.id = ctd.application_termination_contract_id END
AND ct.accepted IS NOT NULL AND ct.rejected IS NULL AND ct.canceled IS NULL
AND ct.termination_type IN ('TRANSFER', 'PAYMENT', 'FULL', 'LIFE_SITUATION')
)
SELECT coalesce(jsonb_agg(to_jsonb(b) ORDER BY b.load_snv_id), '[]'::jsonb)
INTO v_mapped FROM base b;
FOR v_row IN
SELECT l.id FROM jsonb_populate_recordset(NULL::ourpension.load_snv, v_source) l
LOOP
SELECT count(*) INTO v_count
FROM jsonb_to_recordset(v_mapped) b(load_snv_id integer)
WHERE b.load_snv_id = v_row.id;
IF v_count <> 1 THEN
v_errors := v_errors || jsonb_build_array(jsonb_build_object(
'load_snv_id', v_row.id, 'matches', v_count,
'error', 'Требуется ровно одно соответствие клиенту, участнику и принятому заявлению'
));
END IF;
END LOOP;
FOR v_bad IN
SELECT b.individual_id, b.year, jsonb_agg(b.load_snv_id) AS row_ids
FROM jsonb_to_recordset(v_mapped)
b(load_snv_id integer, individual_id integer, year integer)
GROUP BY b.individual_id, b.year HAVING count(*) > 1
LOOP
v_errors := v_errors || jsonb_build_array(jsonb_build_object(
'load_snv_ids', v_bad.row_ids,
'error', 'Несколько справок одного клиента за один год: они аннулируют друг друга'
));
END LOOP;
FOR v_bad IN
SELECT b.load_snv_id FROM jsonb_to_recordset(v_mapped)
b(load_snv_id integer, sharer_id_1c text)
WHERE b.sharer_id_1c IS NULL OR btrim(b.sharer_id_1c) = ''
LOOP
v_errors := v_errors || jsonb_build_array(jsonb_build_object(
'load_snv_id', v_bad.load_snv_id, 'error', 'Не определён счёт 1С для расчёта взносов'
));
END LOOP;
IF jsonb_array_length(v_errors) > 0 THEN
RAISE EXCEPTION USING
MESSAGE = format('СНВ: неполный или неоднозначный набор файла %s; регистрация не начата', p_file_id),
DETAIL = v_errors::text;
END IF;
-- Расчёт из модели SN0s1Sgg: один запрос операций на пару счёт/вид.
WITH base AS (
SELECT * FROM jsonb_to_recordset(v_mapped) b(
load_snv_id integer, application_termination_contract_id integer,
individual_id integer, sum_tax_deduction numeric, year integer,
type text, service_type text, sharer_id_1c text
)
), account_pairs AS (
SELECT DISTINCT b.sharer_id_1c, b.service_type FROM base b
), operations AS (
SELECT p.sharer_id_1c, p.service_type,
date_part('year', t.period_date)::integer AS year, t.amount
FROM account_pairs p
JOIN mgr.ft_dwh_get__pension_account_operation(jsonb_build_object(
'p_sPensionAccountId', p.sharer_id_1c, 'p_sServiceType', p.service_type
)) t ON true
WHERE CASE WHEN p.service_type = 'NPO'
THEN t.operation_type_name IN ('Ч/з банк', 'От работодателя')
WHEN p.service_type = 'PDS'
THEN t.operation_type_name = 'Сберегательные взносы'
ELSE false END
), sum_by_year AS (
SELECT o.sharer_id_1c, o.service_type, o.year, sum(o.amount) AS value
FROM operations o GROUP BY o.sharer_id_1c, o.service_type, o.year
), prepared AS (
SELECT b.*, coalesce(s.value, 0) AS sum_income,
m.value AS max_tax_deduction
FROM base b
LEFT JOIN sum_by_year s ON s.sharer_id_1c = b.sharer_id_1c
AND s.service_type = b.service_type AND s.year = b.year
LEFT JOIN ourpension.max_year_tax_deduction m
ON m.year = b.year AND coalesce(s.value, 0) >= 0
)
SELECT jsonb_agg(to_jsonb(p) ORDER BY p.load_snv_id)
INTO v_ready FROM prepared p;
-- Проверяем также возможное размножение строк справочником лимитов.
FOR v_row IN
SELECT l.id FROM jsonb_populate_recordset(NULL::ourpension.load_snv, v_source) l
LOOP
SELECT count(*) INTO v_count FROM jsonb_to_recordset(v_ready)
b(load_snv_id integer) WHERE b.load_snv_id = v_row.id;
IF v_count <> 1 THEN
RAISE EXCEPTION 'СНВ: для строки % файла % получено % наборов параметров',
v_row.id, p_file_id, v_count;
END IF;
END LOOP;
FOR v_row IN
SELECT b.individual_id, b.year FROM jsonb_to_recordset(v_ready)
b(individual_id integer, year integer)
ORDER BY b.individual_id, b.year
LOOP
PERFORM pg_advisory_xact_lock(hashtext('ourpension.snv_auto.person_year'),
hashtext(v_row.individual_id::text || ':' || v_row.year::text));
END LOOP;
v_batch_id := nextval('ourpension.year_tax_deduction_batch_sequence')::integer;
v_version_datetime := clock_timestamp() AT TIME ZONE 'Europe/Moscow';
FOR v_row IN
SELECT * FROM jsonb_to_recordset(v_ready) b(
load_snv_id integer, application_termination_contract_id integer,
individual_id integer, sum_tax_deduction numeric, year integer,
type text, sum_income numeric, max_tax_deduction numeric
) ORDER BY b.load_snv_id
LOOP
-- p_date NULL воспроизводит отсутствующую row.date в ручной модели.
-- Текущее время создания задаётся явно последним аргументом.
-- Сумма -1, как и в операции qu84AJSQB, означает NULL.
PERFORM ourpension.f_merge_year_tax_deduction(
v_row.application_termination_contract_id::integer, NULL::date,
v_row.individual_id::integer, v_row.max_tax_deduction::integer,
v_row.sum_income::numeric, NULLIF(v_row.sum_tax_deduction, -1)::numeric,
v_row.type::text, v_row.year::integer, p_username::text,
p_user_id::integer, v_batch_id::integer, NULL::integer,
v_version_datetime::timestamp
);
-- Возврат функции всегда 0; проверяем реальный результат записи.
SELECT count(*) INTO v_count FROM ourpension.year_tax_deduction y
WHERE y.batch_id = v_batch_id AND y.individual_id = v_row.individual_id
AND y.year = v_row.year
AND y.application_termination_contract_id = v_row.application_termination_contract_id
AND y.type::text = v_row.type AND y.canceled IS NULL
AND y.accepted IS NOT NULL;
IF v_count <> 1 THEN
RAISE EXCEPTION 'СНВ: не подтверждена регистрация строки % файла %',
v_row.load_snv_id, p_file_id;
END IF;
v_registered_count := v_registered_count + 1;
END LOOP;
-- Исходный UPDATE из TMbTo05q выполняется только после ВСЕХ справок.
UPDATE report.fns_reference_request_journal fns
SET response_received = true
FROM jsonb_populate_recordset(NULL::ourpension.load_snv, v_source) snv,
dev.model_file_to_verification mfc, report.smev_register sr
WHERE fns.model_file_to_verification_id = mfc.id
AND mfc.register_id = sr.id
AND fns.contract_number = snv.contract_number
AND sr.number = snv.request_number::text
AND fns.is_sent_to_fns = true
AND snv.file_id = p_file_id;
GET DIAGNOSTICS v_journal_updated_count = ROW_COUNT;
-- Повторяем контроль сопоставления: нулевой UPDATE не является успехом.
FOR v_row IN
SELECT l.* FROM jsonb_populate_recordset(NULL::ourpension.load_snv, v_source) l
LOOP
IF NOT EXISTS (
SELECT 1 FROM report.fns_reference_request_journal fns
JOIN dev.model_file_to_verification mfc
ON mfc.id = fns.model_file_to_verification_id
JOIN report.smev_register sr ON sr.id = mfc.register_id
WHERE fns.contract_number = v_row.contract_number
AND sr.number = v_row.request_number::text
AND fns.is_sent_to_fns = true AND fns.response_received = true
) THEN
RAISE EXCEPTION 'СНВ: отметка ответа не подтверждена для строки % файла %',
v_row.id, p_file_id;
END IF;
END LOOP;
IF v_registered_count <> v_source_count OR v_journal_updated_count < 1 THEN
RAISE EXCEPTION 'СНВ: неполный результат регистрации файла %', p_file_id;
END IF;
v_result := jsonb_build_object(
'success', true, 'already_processed', false, 'file_id', p_file_id,
'batch_id', v_batch_id, 'source_count', v_source_count,
'registered_count', v_registered_count,
'journal_updated_count', v_journal_updated_count,
'version_datetime', v_version_datetime
);
INSERT INTO ourpension.snv_auto_file_processing(
file_id, batch_id, registered_count, journal_updated_count,
version_datetime, username, user_id, result
) VALUES (
p_file_id, v_batch_id, v_registered_count, v_journal_updated_count,
v_version_datetime, p_username, p_user_id, v_result
);
RETURN v_result;
-- Не перехватываем ошибку как успех: исключение откатывает весь вызов.
END;
$function$
;