От ручного расчёта в Telegram-переписке к системе, которой доверяет клиент: автоматический расчёт премий


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


Контекст проекта

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

Исходная ситуация: ручной расчёт без единой точки истины

К моменту, когда я подключилась, задача прошла первый раунд согласований. Первоначальные требования не пережили первую же встречу с клиентом: поменялся триггер расчёта, источник участников и требование «сумма процентов = 100%». Премии до этого считались вручную: единой точки истины не было, расходы на подрядчиков лежали в Telegram-переписке, а не в CRM. Дальнейший анализ, проектирование решений и реализация — мой этап.

Анализ задачи: где требования расходились с реальностью

Первым делом прошлась по реальной структуре CRM, а не по прототипу интерфейса. Оказалось, что часть «новых» полей уже существовала — поле статуса услуги, которое в прототипе называлось иначе, на деле было в модуле инвойсов под своим именем. Это сразу сняло необходимость создавать лишние сущности.

Второе — разобрала противоречие в требованиях. В документах проекта в нескольких местах фигурировало условие «сумма процентов участников = 100%», но было и прямо противоречащее ему указание: каждый участник получает свой процент независимо от других. Я разобрала обе версии и зафиксировала рабочую модель — независимые проценты, остаток идёт в прибыль компании. Это не техническая правка, а другая финансовая модель: если бы её не поймали на этом этапе, PM-ы получали бы неправильные суммы задним числом.

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

Ключевые решения

1. PM = владелец сделки, а не владелец инвойса — фиксированные 5%. Черновая логика брала за PM владельца инвойса в CRM, а им на практике часто оказывался администратор, просто заводивший документ в систему. Мы с клиентом разобрали развилку: цена ошибки высокая — премия ушла бы не тому человеку, а после выплаты в системе выплат это уже не откатить, только исправлять вручную задним числом. Решили в пользу владельца сделки с фиксированными 5%, редактируемыми вручную в виджете.

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

3. Независимые проценты участников — не требование «в сумме 100%». После того как я нашла прямое противоречие между версиями требований, зафиксировала модель: каждый участник получает свой процент от расчётной базы независимо от остальных, остаток — прибыль компании. Это меняет саму механику расчёта, а не просто формулировку.

4. Блокировка расчёта до готовности справочника сотрудников. Я предложила это условие, увидев риск: если начать считать премии до того, как в системе управления кадрами выставлены все отделы и сотрудники, часть расчётов придётся переделывать вручную. Ввели жёсткое правило — расчёт полностью заблокирован, пока клиент не подтвердит, что справочник готов. Это решение не было в исходном ТЗ — оно появилось, когда стало ясно, к чему приведёт запуск без него.

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

Что построила: три интерфейса для расчёта и выплаты бонусов

Три интерфейса под разные роли и сценарии использования:

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

Виджет на карточке сделки — сводный, только для просмотра: сколько инвойсов по сделке рассчитано, сколько ожидают, какой суммарный фонд премий.

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

Уже в процессе выяснилось, что российский офис не подключён к системе выплат — данные туда нужно передавать в бухгалтерию в привычном формате. Я решила это добавлением двух кнопок выгрузки в Excel: первая — готовая ведомость для начисления (строка: сотрудник — период — сумма), вторая — детальный расчёт для анализа с разбивкой по инвойсам и участникам. Обе кнопки работают для всех групп сотрудников, не только для российского офиса — на случай если клиенту понадобится сохранить данные в привычном виде помимо автоматической выгрузки в систему выплат.

В основе — CRM как источник финансовых фактов, серверная платформа (Python + реляционная база данных) как расчётный движок и кэш, и три системы на периферии: система бухгалтерского учёта для налоговых сумм, система управления кадрами для хранения начислений и справочника, два инстанса системы выплат — по одному на каждое юрлицо.

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

Технические сложности при автоматизации расчёта премий

Ложное убеждение про поле с налогом — и как оно держало лишнюю архитектуру. В проекте сложилось убеждение, что поле с суммой налога в CRM API «всегда возвращает 0», под это была выстроена отдельная интеграция с системой бухгалтерского учёта за суммой налога. Когда я разобрала сырой ответ API построчно, оказалось: поля с таким именем в модуле вообще не существует — молчаливый пустой ответ, не ошибка. Нужные данные всё это время были в CRM под другим именем. Находка открывает возможность упростить часть интеграции с системой бухгалтерского учёта — пока решили не трогать без отдельного ревью.

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

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

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

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

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

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

Результат: от неформального процесса к управляемой системе

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

11 дней — это не только про скорость разработки. Это ещё и про клиента. Команда клиента провела отдельную сессию вопросов и ответов, закрыв девять спорных моментов за один присест, трижды приходила смотреть на живую систему с конкретными правками — в том числе настояв, что процент PM должен уметь быть нулевым и что один человек может фигурировать в расчёте дважды под разными ролями. На каждую итерацию давали обратную связь быстро, приходили на встречи подготовленными, принимали решения на встрече, а не после. Это не само собой разумеется. Такой темп невозможен, если клиент затягивает ответы, согласовывает решения внутри неделями или приходит на демо неподготовленным. Хорошее внедрение — это всегда совместная работа.

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

Принципы в действии

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

— Искать дублирование логики между фоновыми и ручными путями явно. Если два куска кода решают похожую задачу, чинить баг нужно проверяя оба места — фикс не распространяется сам.

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

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

— Переиспользование архитектурного паттерна — не лень, а зрелость. Если для похожей задачи уже есть рабочий паттерн, конструировать новую архитектуру с нуля — трата времени, а не тщательность.

— Закладывать в архитектуру то, что ещё не появилось, но появится точно. Второй инстанс системы выплат отсутствовал до самого финала — но место для него было в архитектуре с первого дня. Когда он появился за три дня до дедлайна, это не стало кризисом.

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

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

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


Стек

Zoho CRM · Zoho Catalyst (Python) · Zoho Books · Zoho People · Zoho Payroll


Опубликовано с разрешения компании.