Перейти к содержимому
[ рис. my-expenses-bot ]

Мой бот для расходов: настроил однажды и больше не записываю покупки

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

Теперь после оплаты картой банк присылает SMS на мой iPhone, телефон передаёт сообщение на сервер, а небольшая программа создаёт запись в Notion, сразу указывая категорию и месяц. Настройку я сделал один раз, и, когда мне хочется понять, куда ушли деньги, я открываю таблицу расходов или прошу своего ассистента Hermes прочитать её. Расходы я по-прежнему проверяю, но записывать каждую покупку мне больше не приходится, и именно поэтому у такой системы есть шанс не быть заброшенной.

  • An illustration made for this piece: a phone under a bank message bubble, with one ink line running out of the phone and away to the right.

Сначала телефон должен передать сообщение

Банк уже собирает для меня исходные данные: в сообщении о покупке есть сумма, обычно есть дата и название продавца, хотя строкой готовой таблицы такое SMS, конечно, не выглядит. Сообщение получает только мой iPhone, поэтому начинать нужно с него: сервер не может отреагировать на текст, которого никогда не видел.

В приложении «Команды» я создал личную автоматизацию с триггером Message, выбрал банк отправителем и добавил фильтр Message Contains, чтобы сценарий запускался по уведомлениям о транзакциях, а не по любому сообщению банка. Автоматизация срабатывает сразу, без запроса подтверждения, а действие Get Contents of URL отправляет POST-запрос на `/expense` моего сервера: в теле JSON лежит исходное сообщение в поле `text`, в заголовке запроса лежит токен авторизации. Фильтры по отправителю и содержимому описаны в руководстве Apple по триггерам «Команд», а отправка POST-запроса с JSON разобрана в руководстве по API-запросам.

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

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

Где в этой истории Hermes

Hermes помог мне связать передачу с телефона, постоянно работающий сервер с обработчиком на Python, доступ к Notion и правила, по которым текст банковского SMS превращается в поля записи. Если продавец попал не в ту категорию, я могу объяснить, куда он должен попадать, а Hermes поможет поправить правило для следующих покупок и исправить уже созданные записи. Когда мне нужен ответ о тратах, ассистент читает актуальные данные Notion, а не пытается угадать, что я когда-то внёс вручную.

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

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

Что делает сервер

Телефон отправляет JSON на `POST /expense`, а сервер принимает запрос только с bearer-токеном; без токена он его отклоняет. Программа `expense_bot.py` работает как служба `expense-bot.service`, и systemd перезапускает её, если процесс завершится. Сам токен я здесь не привожу, но есть и другая важная оговорка: мой телефон обращается к серверу извне, поэтому тому, кто захочет повторить схему, нужно позаботиться не только об авторизации, но и о защите соединения. Сейчас у меня прямое соединение по HTTP, и я не предлагаю копировать его как образец безопасности: прежде чем советовать такую настройку другому человеку, я бы поставил перед сервером TLS.

На запрос телефон получает быстрый ответ, а расход сервер обрабатывает уже после него, потому что «Команды» на iOS не будут бесконечно ждать поиска продавца и обращения к Notion. Так телефон не упирается в тайм-аут, но ответ «обрабатываю» ещё не означает, что запись сохранилась: о неудачной записи в Notion я узнаю из лога службы, а не из уведомления на iPhone. Когда чего-то недостаёт в таблице, я проверяю и её, и службу; полностью избавить меня от внимания эта схема не может.

При разборе SMS программа берёт первое положительное число с двумя цифрами после десятичного разделителя, ищет дату, если она есть в сообщении, и извлекает продавца после банковского `at`. Если даты в тексте нет, используется текущая дата сервера по UTC, а если банк поменяет формат SMS, эти допущения придётся пересмотреть: найденное регулярным выражением число ещё не обязательно та сумма, которую я хотел учесть.

Затем срабатывают отдельные правила, и только после них более общие: Careem Quik и Noon Minutes относятся к продуктам, Careem Food к еде вне дома, а Careem Plus к подпискам. Для остальных знакомых продавцов программа проверяет названия и ключевые слова категорий; если продавец неизвестен, она ищет его название с привязкой к Дубаю и смотрит, не указывают ли результаты на ресторан, супермаркет, аптеку или заправку. Если определить категорию всё равно не удаётся, запись получает запасную категорию, а не пропадает. Категоризация происходит без меня, но это не значит, что она всегда верна, и от меня больше не требуется выбирать категорию при каждой покупке.

[ рис. Таблица правил, в которой одна запись неверна, и починка занимает одну строку: ошибка повторяется одинаково, а не меняет форму. ]

В Notion хранятся записи, а не форма для ручного ввода

Бот создаёт строку в базе Expenses: в её названии стоят продавец и сумма, отдельно записываются сумма в дирхамах AED числом, дата транзакции и время. Строка связывается со страницей нужной категории в Budget и со страницей своего месяца в Month Classification, причём связь с бюджетом задаётся при создании строки, а связь с месяцем добавляется сразу после. Благодаря этим связям отдельные банковские сообщения складываются в таблицу расходов, которую можно читать по категориям и месяцам.

Для этого пришлось один раз настроить в Notion базу Expenses с нужными полями, страницы категорий в Budget, страницы месяцев в Month Classification и интеграцию с доступом к соответствующим базам. Боту нужен действующий токен Notion, а страница месяца должна существовать заранее: если её нет, программа пишет предупреждение в лог и оставляет связь с месяцем пустой. Для неизвестной категории предусмотрен запасной вариант, но создать недостающую страницу месяца программа не пытается.

На страницах Budget указаны месячный бюджет, собранная из связанных расходов сумма за текущий месяц и формула, показывающая, какая доля бюджета израсходована. Страницы Month Classification собирают расходы по месяцам, рядом с полями дохода и месячного остатка, так что мне не нужно по воскресеньям переносить цифры в другую таблицу ради отчёта. Я могу сравнить траты по категории с бюджетом, открыть нужный месяц или спросить Hermes о недавних расходах, опираясь на уже записанные данные. А вот решать, стоила ли покупка своих денег, я хочу сам.

Что значит «настроил однажды»

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

  • An illustration made for this piece: a magnifying glass resting on a ruled page, the one habit that stays.

ИИ здесь не знает о моих финансах больше, чем я: он помог мне построить и поддерживать небольшой переход от данных, которые и так приходят на телефон, к вопросам, которые я захочу задать позже. Теперь категория выбирается без ещё одного нажатия, сумма сохраняется без ещё одного открытия приложения, а моё внимание остаётся для проверки трат, а не для переписывания SMS. Мне этого достаточно: один раз наладить рутинную работу и оставить решение за собой.