Retroactive rate change: что происходит при ставке задним числом
VP of Engineering выходит после Q1-ревью и объявляет 8% повышение для 12 backend-инженеров с эффективной датой 1 марта. На календаре 18 мая. Три месяца отчётов уже улетели в борд со старыми ставками. У HR два варианта: сделать вид, что повышение начинается сегодня, или ретроактивно обновить март, апрель и май. Большинство engineering finance инструментов вынуждают выбрать первый. PanDev Metrics поддерживает второй, и Sarbanes-Oxley Act 2002 — причина, почему делать это нужно очень аккуратно.
Это одна из немногих областей, где наш продукт реально расходится с LinearB, Jellyfish и Code Climate Velocity. Те инструменты построены на forward-only моделях ставок. Таблица UserRate в PanDev битемпоральная: у каждой ставки есть startPeriod и endPeriod, и OverheadCoefficientFullRecalcCronJob переигрывает activity-события через новую ставку × overhead K, когда исторические строки меняются. Это мощно. И это ровно та функциональность, на которую аудиторы смотрят дважды.
{/* truncate */}
Полный каскад от одной строки UserRate с датой в прошлом. Все зависимые cost-строки переигрываются.
Почему большинство инструментов отказываются это делать
Cost-модуль LinearB использует месячный snapshot. Как только февраль закрыт, ставка, активная на момент построения отчёта, замораживается. То же у allocation-движка Jellyfish. Их логика обоснована: если разрешить редактировать историю, можно отредактировать «выход» из плохих чисел.
Проблема в том, что реальная компенсация не уважает дату закрытия. Циклы salary review идут квартально или раз в полгода, потом аппрувы стопкой лежат в HR, потом новая ставка коммуницируется через несколько недель с ретроактивным эффектом. WorldatWork 2024 Salary Increase Survey сообщает, что 62% американских компаний делают retroactive effective date минимум на одном merit cycle в год, со средним смещением 47 дней. Если ваша finance-система это не моделирует, ваши «фактические данные» систематически неверны на четверть квартала — каждый квартал.
Дизайн PanDev сделан так, чтобы ретроактивность была нативной, а audit trail — громким. Вы можете менять историю. Каждое изменение timestamp-нуто, привязано к пользователю и видно в панели версий ставки. SOX Section 404 требует «internal controls over financial reporting» — это ровно то, чем audit log и является.
Как UserRate устроен внутри
У каждого пользователя список строк ставок в таблице UserRate:
user_rate (
id,
user_id,
department_id,
amount,
rate_type ENUM('HOURLY', 'MONTHLY'),
start_period DATE,
end_period DATE, -- nullable; NULL = текущая
created_by,
created_at
)
У сотрудника с одной MONTHLY-ставкой $5,000 от 1 января 2026 одна строка с start_period = 2026-01-01, end_period = NULL. Когда HR заводит новую ставку с 1 марта на $5,400, PanDev атомарно делает две вещи:
- Закрывает существующую строку —
end_period = 2026-02-28. - Вставляет новую строку с
start_period = 2026-03-01,end_period = NULL,amount = 5400.
Если новая строка датирована в прошлом относительно сегодняшнего дня, система помечает её как retroactive insert и ставит в очередь OverheadCoefficientFullRecalcCronJob для затронутых (department_id, year, month).
Каскад пересчёта, шаг за шагом
Когда cron подхватывает задачу пересчёта, цепочка отрабатывает механически и исчерпывающе.
Таблица 1 — Что и где меняется при retroactive rate
| Шаг | Сущность пересчитывается | Зачем |
|---|---|---|
| 1 | История UserRate закрыта/вставлена | Новая строка отражает ретро-повышение |
| 2 | OverheadCoefficient per (dept, year, month) | taskTotal и nontaskTotal растут, потому что direct dev cost вырос |
| 3 | mv_cost_per_task_daily materialized view | Loaded cost каждой задачи переигрывается через новую ставку × новый K |
| 4 | mv_cost_per_feature_monthly materialized view | Feature-rollups обновляются из обновлённых cost-задач |
| 5 | Department / project cost-отчёты | Агрегации обновляются из нового состояния MV |
| 6 | Plan-vs-Actual variance line RETRO_RATE_DELTA | Variance декомпозируется, чтобы ретро-изменение не загрязняло scope-creep |
| 7 | Запись в audit log | User, timestamp, before/after rate, dollar delta по каждому затронутому месяцу |
Пересчёт идемпотентен: повторный запуск на том же диапазоне даёт те же числа. Это non-negotiable свойство для SOX, где аудиторы должны воспроизвести цифры из raw-событий.
UI: где HR это реально делает
Поток /dashboard/employees/salary-rate/:employeeId. Панель истории показывает все предыдущие строки ставок. Диалог «New Rate» спрашивает четыре вещи: эффективная дата, сумма, тогл HOURLY/MONTHLY и обязательное текстовое обоснование. Перед confirm диалог рендерит preview impact: сколько уже финализированных месяцев будет затронуто, dollar delta на taskTotal и были ли какие-то из этих месяцев «locked» finance-закрытием.
Последний пункт — ключевой контроль. Месяцы в статусе LOCKED_PERIOD (выставленном Finance после month-end close) требуют дополнительного аппрува: финансовая роль должна co-sign ретро-изменение. Дефолтный конфиг требует двух подписей на любое ретро, пересекающее границу locked-месяца. AICPA Statement on Auditing Standards 122 считает это правило двух человек базовым ожиданием для любого restatement workflow.
Worked example: ретро-повышение на 8%
Возвращаемся к VP of Engineering. 12 backend-инженеров, средний direct rate $42/час, 8% повышение с 1 марта, обработано 18 мая. K департамента: 0.41 в марте, 0.43 в апреле, 0.45 в мае (сезонность мы разбирали в per-month K).
HR заводит 12 новых строк UserRate с start_period = 2026-03-01. Cron ставит в очередь пересчёт (dept_id=engineering, 2026, 3..5). За ~90 секунд каскад завершается.
Таблица 2 — Cost-отчёт за 31 марта: оригинал vs replayed
| Cost line | Запуск 1 апреля (до ретро) | Запуск 19 мая (после ретро) | Delta |
|---|---|---|---|
| Direct dev cost (12 инженеров, март) | $80,640 | $87,091 | +$6,451 |
taskTotal (март) | $58,866 | $63,576 | +$4,710 |
nontaskTotal (март) | $21,774 | $23,515 | +$1,741 |
| Overhead K (март) | 0.410 | 0.405 | -0.005 |
| Loaded cost per task hour | $59.22 | $63.74 | +$4.52 |
| Total department cost (март) | $113,702 | $122,562 | +$8,860 |
| Total cost (март+апрель+май) | $345,815 | $373,732 | +$27,917 |
Два момента. Первое: K немного упал, потому что adminSalary (CTOs, EMs, DevOps) не изменился, а taskTotal + nontaskTotal вырос — overhead стал меньшей долей увеличившегося знаменателя. Второе: total delta $27,917 — не сюрприз бюджету: это ровно 8% от direct cost тех 12 инженеров за три месяца плюс K-blended overhead reweighting. Цифра сходится.
CFO, запускающий cost-отчёт за 31 марта сегодня, получит $122,562. Тот же отчёт, запущенный 1 апреля, давал $113,702. Оба корректны. Audit log объясняет разницу одним предложением: «Retroactive rate increase 8% applied to 12 engineers (employee_ids 401–412) on 2026-05-18 by user a.pan@pandev.io.»
Как это связано с остальным cost-стеком
Каскад пересчёта работает только потому, что весь cost-движок битемпоральный. Если вы читали loaded hourly rate, знаете, что loaded rate = direct × (1 + K), а сам K зависит от тех же taskTotal/nontaskTotal, что только что пересчитались. Если читали overhead coefficient, знаете, что K — месячный per department, никаких усреднений.
Ретро-пересчёт уважает оба правила: каждый месяц получает свой K replay, каждый loaded rate перестраивается из нового direct rate. Поэтому это не могло быть простым шорткатом «умножить затронутые отчёты на 1.08». K сдвигается, когда сдвигается direct cost, поэтому распространение реально переигрывает event-данные, а не патчит итоги.
Где живёт audit trail
PanDev держит три слоя evidence для любого ретро-изменения ставки:
- Версионирование
UserRate— старая строка остаётся со своим закрытымend_period; новая существует рядом. Можно спросить «какая ставка была у Марии 15 марта?» против любого исторического timestamp и получить ответ, который был корректен на тот момент. AuditLogEntry— append-only таблица сentity = 'UserRate',action = 'RETRO_INSERT',actor,before_state,after_state,dollar_delta_per_month,approver(если сработал dual-control).- Variance attribution — Plan-vs-Actual отчёт добавляет отдельную строку
RETRO_RATE_DELTA, чтобы ретро-изменение не смешивалось со scope creep или сезонными K-spike. Зачем эта строка нужна, разбирали в budget variance analysis.
Аудиторы при SOX 404 attestation обычно задают три вопроса: кто поменял число, когда и с чего на что. Три слоя выше отвечают на все три из одного и того же event log.
Чем это не является
Retroactive rate change — мощный инструмент, который можно использовать неправильно. Backdating ставок ради манипуляции квартальными цифрами — ровно тот сценарий, для отлова которого SOX и был написан. См. enforcement actions SEC по revenue restatement. Audit trail здесь ваш друг, не щит. Каждое ретро-изменение timestamp-нуто, attribut-но и видно любому с audit-ролью. Не используйте эту фичу, чтобы «причесать» плохие числа. Используйте, чтобы держать хорошие числа точными.
Несколько паттернов, которых не должно быть и которые система флажит:
- Ретро-снижение ставки прямо перед расчётом контрактного бонуса.
- Ретро-изменение, пересекающее границу финансового года без finance-аппрува.
- Ретро-изменение на собственном аккаунте сотрудника от его же менеджера (self-approval заблокирован).
HR Technology Conference 2024 проводила панель отдельно по compensation transparency tooling, и консенсус панели — перефразированный из опубликованного recap — был, что видимость ретро-изменения важнее, чем его предотвращение. Людям иногда придётся датировать задним числом. Плохая версия — это невидимое backdating.
Honest limit: что это не чинит
Каскад пересчёта держит cost-отчёты внутренне консистентными. Он не чинит ретроактивно решения, принятые на старых числах. Если CFO презентовал Q1-результаты борду 15 апреля по pre-retro ставкам, эти числа были корректны на 15 апреля. Сейчас они устарели. Материалы для борда не перегенерируются. Это отдельная workflow — обычно addendum к следующему квартальному ревью с пометкой «Q1 dev cost restated upward by $19K following May salary review; restatement does not affect operating margin classification.»
PanDev даёт данные, чтобы написать этот addendum. Написать его за вас не может. И не может откатить board commitment, сделанный на числах, которые с тех пор были restated.
Финальная позиция, которую готов защищать
Если ваш engineering finance инструмент не поддерживает retroactive rate changes с полным audit propagation — у вас на самом деле нет engineering finance. У вас engineering bookkeeping с проблемой квартальных корректировок. Интересный вопрос не в том, разрешать ли ретро-изменения — реальность payroll их форсирует. Вопрос в том, производит ли система, которая их абсорбирует, аудит-готовую историю или тихо перезаписывает её. Выбирайте ту, что не делает вид, что прошлое неизменяемо.
