В выписке лежит поступление на 5 200 рублей. В магазине есть два неоплаченных заказа ровно на 5 200. Назначение платежа: «оплата заказа», без номера. Любая автоматизация, написанная на скорую руку, радостно привяжет платёж к первому найденному заказу, закроет его и пойдёт дальше с чувством выполненного долга. Через неделю выяснится, что платил клиент второго заказа, а первый уже отгружен.

Вся сверка держится на одном различии: совпавшая сумма — это арифметика, а подтверждённая оплата — это вывод из нескольких признаков. Сумма может быть одним из признаков, но не доказательством. Поэтому хороший сценарий в n8n не пытается связать всё подряд. Его задача иная: быстро и надёжно сказать про каждую строку выписки одно из трёх — «однозначно этот заказ», «кандидаты есть, нужен человек», «ничего подходящего». Разберём, как это построить, и посмотрим на живом примере из трёх строк выписки и четырёх заказов.

Речь только о разборе файла выгрузки, который вам разрешено получать и обрабатывать. Это не подключение к банку и не платёжные действия: сценарий ничего не списывает, не подтверждает и не отправляет, он лишь раскладывает строки по заказам и помечает сомнительные.

Сначала прочитать файл и ничего не испортить

Выгрузка обычно приходит как CSV или таблица Excel. В n8n такие файлы превращает в JSON узел Extract From File: в документации перечислены форматы CSV, XLS и XLSX, и на выходе каждая строка файла становится отдельным элементом с полями по заголовкам столбцов. Это удобно, потому что дальше вы работаете не с «текстом из банка», а с обычными записями.

Первое правило этого этапа неромантично: исходная строка неприкосновенна. Прежде чем что-либо чистить, сохраните её целиком. Для каждой записи полезно хранить:

  • имя файла и номер строки в нём — чтобы через месяц найти место, откуда пришла запись;
  • все исходные поля как есть, одним JSON-объектом;
  • время загрузки;
  • ключ операции — о нём ниже, он важнее всего остального.

Зачем хранить сырое, если всё равно будет нормализованное? Затем что нормализация — это ваши решения, а решения бывают ошибочными. Если регулярное выражение для номера заказа однажды съело лишнюю цифру, вы должны иметь возможность пересчитать всё с оригинала, а не просить бухгалтерию выгрузить файл заново и надеяться, что он совпадёт.

Нормализация рядом с оригиналом, а не вместо него

Банковские выгрузки гостеприимны к разнообразию. Даты приходят в формате «день.месяц.год», иногда со временем, иногда без. Суммы — с запятой или точкой, с пробелом-разделителем тысяч, иногда в кавычках. Назначение платежа — свободный текст, в который человек мог вписать что угодно: «заказ №1041», «з-з 1041», «оплата по сч. 1041 от 03.11», а иногда просто «за товар».

Рядом с каждым исходным полем заведите нормализованное:

  • дата — в обычный тип даты; исходная строка остаётся в сыром объекте;
  • сумма — целым числом в минимальных единицах (копейках). Так вы исключаете ошибки округления дробных чисел и сравниваете суммы на строгое равенство, а не «почти»;
  • назначение — копия в нижнем регистре, с схлопнутыми пробелами и единым видом разделителей; оригинал не трогаем;
  • извлечённые номера заказов — отдельный список кандидатов, найденных в тексте назначения.

Последний пункт заслуживает отдельного внимания: из текста вы получаете не «номер заказа», а список номеров, похожих на номер заказа. Если формат ваших номеров — четыре цифры, то в «оплата по счёту 2217 за заказ 1041» нашлись два числа, и только знание ваших данных подскажет, какое из них заказ. Поэтому на этом этапе нельзя отбрасывать лишнее молча: сохраните все найденные варианты и дайте следующему шагу решить, какой из них существует в базе заказов.

Пустые значения тоже требуют дисциплины. Если в выгрузке нет номера документа, сохраните это как настоящее отсутствие значения (NULL), а не как пустую строку. В PostgreSQL это разные вещи: пустая строка не равна NULL. Если же вы перемешаете их, ограничения уникальности и условия поиска начнут вести себя не так, как вы ожидаете, причём тихо.

Три строки, четыре заказа

Возьмём условный пример: числа придуманы для объяснения. В выписке за день три поступления:

Строки выписки
СтрокаСумма, ₽Назначение
R112 400,00Оплата заказа 1041
R25 200,00Оплата заказа (номер не указан)
R312 400,00По заказам 1043 и 1044

В базе магазина четыре неоплаченных заказа:

Заказы в ожидании оплаты
ЗаказСумма, ₽
104112 400,00
10425 200,00
10437 200,00
10445 200,00

Строк три, заказов четыре, и количества не сходятся ни по какой схеме «один к одному». Это не искусственная сложность: реальная выписка устроена именно так, потому что клиенты платят как хотят.

  • R1 — самый приятный случай. Номер 1041 найден в назначении, такой заказ существует и ожидает оплаты, сумма равна сумме заказа. Два независимых признака согласуются.
  • R2 — сумма подходит сразу двум заказам, 1042 и 1044, а номера в тексте нет. Тут и спрятана ловушка из начала статьи: автоматика не имеет права выбирать.
  • R3 — сумма 12 400 совпадает с заказом 1041, но в назначении названы 1043 и 1044, и 7 200 + 5 200 как раз дают 12 400. Это общая оплата двух заказов. При этом по одной только сумме её легко принять за дубль платежа по 1041.

Теперь заметьте тонкость. Если R3 закрывает заказ 1044, то для R2 остаётся единственный подходящий кандидат — 1042. Заманчиво считать это решением. Но вывод «методом исключения» опирается на то, что R3 разнесена верно, и на то, что клиент R2 вообще платил за один из этих заказов. Такой результат разумнее помечать как «вероятно» и отправлять на подтверждение, а не закрывать молча.

Основания сопоставления: от сильных к слабым

Вместо одного условия «сумма равна» заведите несколько оснований и записывайте, какое из них сработало. Примерная лестница:

  1. Номер заказа в назначении + совпадение суммы. Самое сильное основание из доступных в текстовой выгрузке. Если номер найден ровно один и сумма сошлась — автоматическая привязка допустима.
  2. Номер заказа найден, сумма не совпадает. Не привязка, а сигнал: частичная оплата, переплата или опечатка в номере.
  3. Несколько номеров в назначении, сумма равна сумме их заказов. Кандидат на общую оплату. Разнесение по заказам допустимо, если все заказы существуют и сумма сошлась точно.
  4. Только сумма (и, при наличии, плательщик). Слабое основание. Если подходящий заказ ровно один — это в лучшем случае «вероятно», если несколько — спорный случай.
  5. Ничего подходящего. Поступление остаётся неразнесённым и попадает в очередь.

Плательщик — отдельный полезный признак, но только если он у вас есть в выгрузке и в заказах в сопоставимом виде. Названия организаций пишут по-разному, поэтому в роли решающего основания он плох, а в роли дополнительного — хорош: помогает человеку быстрее выбрать из кандидатов.

Как это ложится на n8n? Узел Merge умеет объединять два потока по совпадению значений выбранных полей, а его настройки различают совпавшие записи, несовпавшие и все. Для первых двух ступеней лестницы этого достаточно: поток строк выписки с извлечённым номером заказа соединяется с потоком заказов по полю «номер заказа», и вы получаете три выхода — пары, строки без пары и заказы без пары.

Но важно трезво понимать, что делает такое соединение. Merge по полям — это быстрый фильтр «нашлось/не нашлось», а не бухгалтерская сверка. Он ничего не знает о том, что одна строка выписки может закрывать несколько заказов, что заказ может быть оплачен в два приёма, что одну и ту же операцию можно импортировать дважды, и что совпадение по ключу не гарантирует совпадения сумм. Все эти правила вам придётся описать самим — отдельными шагами после соединения. Merge отвечает на вопрос «есть ли пара по этому полю», а решать, достаточно ли этого, будет следующий узел.

Практичный порядок такой: после соединения по номеру проверьте суммы в отдельном шаге и присвойте каждой паре метку основания и уровень уверенности. Для строк без пары запустите вторую, более слабую попытку — по сумме — и сразу посчитайте, сколько кандидатов нашлось: ноль, один или больше одного. Это число и определяет судьбу строки.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Частичная оплата, общая оплата и привычка клиентов делать по-своему

Модель «одна строка выписки — один заказ» ломается именно в этих местах, поэтому устройте данные так, чтобы она не была зашита в схему. Вместо поля «оплаченный заказ» в таблице выписки заведите отдельную таблицу разнесений: каждая запись связывает строку выписки с заказом и хранит часть суммы, которая к нему отнесена.

Тогда все три нестандартных случая описываются одним способом:

  • Обычная оплата — одна запись разнесения на всю сумму строки.
  • Общая оплата — несколько записей для одной строки, например для R3: 7 200 на заказ 1043 и 5 200 на заказ 1044. Сумма разнесений должна равняться сумме строки.
  • Частичная оплата — запись на часть суммы заказа. Заказ считается закрытым, когда сумма его разнесений достигла суммы заказа; пока меньше — это «оплачен частично», и остаток виден.

Неразнесённый остаток строки — тоже информация. Если строка на 12 400, а разнесено 12 000, у вас остаётся 400 неясных рублей, и сценарий не должен их ни выбрасывать, ни «подгонять» к ближайшему заказу.

Теперь вернёмся к R3. Автоматическое разнесение здесь можно разрешить, но только при жёстких условиях: оба номера найдены, оба заказа существуют и не оплачены, а их суммы в сумме дают ровно сумму строки. Если хотя бы одно условие не выполнено — например, нашлось два номера, а сумма равна только одному из заказов, — строка уходит человеку. Заметьте, что тот же R3 по одной сумме выглядел бы платежом за 1041, и именно поэтому порядок оснований важен: номера в назначении проверяются раньше, чем «просто сумма».

Повторный импорт: уникальность вместо надежды

Файл выписки загружают повторно чаще, чем хочется признавать: сценарий перезапустили после сбоя, выгрузили период с наложением, кто-то положил в папку тот же файл под другим именем. Если на входе нет защиты, платёж появится в базе дважды, а оплаченный заказ окажется «переоплаченным».

Защита строится не в n8n, а в базе. Нужен ключ операции — значение, по которому одну и ту же операцию можно узнать при любом импорте, — и ограничение уникальности на нём. В PostgreSQL PRIMARY KEY даёт уникальность и запрещает NULL, а UNIQUE может охватывать сразу несколько столбцов — это пригодится, когда единого идентификатора операции нет.

Откуда взять ключ, зависит от вашей выгрузки. Если в ней есть собственный идентификатор операции или уникальный номер документа, используйте его вместе со счётом. Если нет, придётся составлять ключ из набора полей: дата, сумма в копейках, номер документа, нормализованное назначение. Такой составной ключ хуже, потому что настоящие одинаковые платежи — два одинаковых взноса в один день с одним и тем же текстом — ему неотличимы. Тогда в ключ добавляют порядковый номер повторения внутри файла, но учтите, что при наложении периодов этот номер может «поплыть», и такие случаи стоит проверять глазами.

CREATE TABLE bank_rows (
  id           bigserial PRIMARY KEY,
  op_key       text    NOT NULL,
  source_file  text    NOT NULL,
  source_row   integer NOT NULL,
  booked_on    date,
  amount_minor bigint  NOT NULL,
  purpose_raw  text    NOT NULL,
  purpose_norm text    NOT NULL,
  raw          jsonb   NOT NULL,
  status       text    NOT NULL DEFAULT 'new',
  UNIQUE (op_key)
);

CREATE TABLE allocations (
  id           bigserial PRIMARY KEY,
  bank_row_id  bigint  NOT NULL REFERENCES bank_rows(id),
  order_no     text    NOT NULL,
  amount_minor bigint  NOT NULL,
  basis        text    NOT NULL,
  UNIQUE (bank_row_id, order_no)
);

Вставка строк выполняется с обработкой конфликта по ключу (INSERT ... ON CONFLICT (op_key) DO NOTHING): повторная запись той же операции просто не создаст дубль. В таблице разнесений уникальная пара «строка выписки — заказ» не даст дважды привязать один платёж к одному заказу, даже если сценарий запустится два раза подряд.

Обратите внимание на столбец basis. Он хранит, по какому основанию сделана привязка, и однажды отвечает на самый неприятный вопрос — «почему система решила, что этот платёж за этот заказ». Ответ «потому что так сработало второе правило» лучше, чем «ну, она так посчитала».

Для общей картины сверки заказов и платежей, включая защиту от двойной обработки на стороне магазина, есть отдельный разбор: сверка заказов и платежей в n8n. Здесь же мы остановились на самой выписке и её строках.

Очередь для человека: не «разберись сам», а готовая карточка

Всё, что не прошло по однозначным основаниям, попадает в очередь ручной проверки. Ошибка многих сценариев — выкидывать туда сырую строку с пометкой «не сопоставлено». Человек, получивший такую строку, вынужден повторять всю работу автоматики вручную, и очередь быстро превращается в то самое чтение всей выписки.

Хорошая карточка спорного случая содержит:

  • исходную строку выписки без изменений: дата, сумма, назначение, плательщик, если он есть;
  • причину, по которой автоматика остановилась: «сумма подходит нескольким заказам», «номер найден, сумма не совпала», «номер не найден»;
  • список кандидатов с суммами, датой создания и текущим статусом оплаты;
  • что останется неразнесённым при каждом варианте выбора;
  • ключ операции, чтобы решение сохранилось и повторный импорт не возвращал строку в очередь.

Для нашего примера карточка R2 выглядела бы так: «5 200 ₽, назначение без номера. Подходят заказы 1042 и 1044 (оба 5 200 ₽). Заказ 1044 также упомянут в строке R3, где разнесён с заказом 1043; если R3 подтверждена, вероятнее 1042». Человек видит всю картину за десять секунд и нажимает нужный вариант. Решение записывается как разнесение с основанием «подтверждено вручную», и повторять его больше не придётся.

Куда доставлять такую очередь, решайте под свою команду: таблица в базе с простым представлением, сообщение в рабочий чат со ссылкой на карточку, письмо — подойдёт всё, что не теряется. Важно лишь, чтобы у строки было состояние: ожидает решения, решена, отклонена. Тогда вы всегда видите, сколько вопросов висит открытыми, а не гадаете, дошло ли что-то до адресата.

Где всё это живёт

Для такого проекта нужны n8n, PostgreSQL и место, где они работают постоянно: сценарий должен получать файлы по расписанию, а база — хранить историю выписок. Удобно держать их на собственном сервере с root-доступом, где вы сами решаете, как настроить доступ к данным. На VPS MAATRIX (NVMe, AMD EPYC, root-доступ; локации в публичном предложении — UK, US, RU и FR) в каталоге есть n8n с установкой, точное наличие образа и ОС стоит посмотреть в форме заказа.

По ресурсам нагрузка у такой сверки скромная: обработка файла выписки — это разбор нескольких сотен или тысяч строк и десятки запросов в базу. Для пробного запуска логично начать с небольшого плана — например, Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) или Uab (2 vCPU, 4 ГБ, 80 ГБ) — и смотреть на реальную загрузку по своим файлам. Если вы поднимаете n8n и PostgreSQL на одной машине и объём выгрузок вырастет, переход на Heka (4 vCPU, 8 ГБ, 160 ГБ) или выше решается по замерам, а не по ощущениям. Сами по себе цифры тарифа не означают гарантированной ёмкости: сколько выписок потянет ваш сервер, покажет ваша нагрузка.

Отдельно о данных. Банковская выгрузка — чувствительный материал: в ней суммы, названия плательщиков, иногда реквизиты. Поэтому ограничьте доступ к n8n и базе, не храните файлы в общедоступных папках и не отправляйте содержимое строк туда, куда вам не разрешено. Резервное копирование базы нужно настроить самостоятельно: наличие автоматических копий на уровне тарифа в публичном предложении не заявлено.

Что должно дойти до человека

Если сценарий собран по описанной схеме, ежедневная работа с выпиской сводится к короткому разбору исключений вместо чтения всех строк. Внимания реально требуют только такие случаи:

  • сумма подходит нескольким заказам, а номера в назначении нет (как R2);
  • в назначении названы несколько заказов, и сумма строки не равна сумме их заказов;
  • номер заказа найден, но сумма меньше или больше — частичная оплата или переплата;
  • номер в назначении не соответствует ни одному неоплаченному заказу;
  • строка не нашла кандидатов вообще;
  • повторный импорт показал строку с известным ключом, но другими значениями — файл изменился задним числом.

Всё остальное — однозначные пары, где номер и сумма сошлись, и точно известные дубли, которые база отклонила сама, — человеку смотреть не нужно. Достаточно общего счётчика: сколько строк разнесено автоматически, сколько ждёт решения. Если второе число день за днём растёт, значит, нужно искать причину в данных — формате назначений, схеме нумерации заказов, привычках плательщиков, — а не добавлять ещё одно хитрое правило сопоставления.