Вчера у четырёх подписок наступил срок продления. К утру оплатили одну. В отчёте уже стоит 25%, хотя у остальных ещё есть время. Для подписок с прошлой недели окно оплаты уже закрылось. Если поставить эти цифры рядом, на графике легко увидеть падение раньше, чем его можно измерить.
Я бы считал долю от числа подписок, у которых по графику наступил срок продления в этот день. Засчитываем оплату за следующий период, если она прошла до истечения 48 часов с планового срока. Предоплата тоже считается продлением. Платёж ровно на границе 48 часов считаем поздним. Границу стоит явно записать в правилах отчёта.
В исходных данных нужна одна строка на ожидаемое продление. В ней хранятся плановый срок и время первой успешной оплаты именно за этот период. Если после предоплаты поле expires_at в текущей подписке сдвигается вперёд, старый срок для отчёта придётся брать из истории. Иначе предоплата может исчезнуть из нужного дня целиком.
Вот пример с данными, полными за время до 11 ноября, 00:00 UTC. День определяется по плановому сроку продления. Строка за 9 ноября уже оплачена, но у остальных подписок этого дня 48 часов могли ещё не пройти. Поэтому в отчёт попадут 7 и 8 ноября целиком, а 9 ноября пока подождёт.
WITH renewals(id, due_at, paid_at) AS ( VALUES (101, timestamptz '2026-11-07 09:00+00', timestamptz '2026-11-06 18:00+00'), (102, timestamptz '2026-11-07 10:00+00', timestamptz '2026-11-09 10:00+00'), (103, timestamptz '2026-11-07 11:00+00', timestamptz '2026-11-08 12:00+00'), (104, timestamptz '2026-11-08 11:00+00', NULL), (105, timestamptz '2026-11-08 18:00+00', timestamptz '2026-11-10 17:00+00'), (106, timestamptz '2026-11-09 09:00+00', timestamptz '2026-11-09 09:30+00') ), days(cohort_date) AS ( VALUES (date '2026-11-07'), (date '2026-11-08'), (date '2026-11-09') ), bounds AS ( SELECT cohort_date, CAST(cohort_date AS timestamp) AT TIME ZONE 'UTC' AS starts_at, CAST(cohort_date + 1 AS timestamp) AT TIME ZONE 'UTC' AS ends_at FROM days ), counts AS ( SELECT b.cohort_date, count(r.id) AS due, count(r.id) FILTER ( WHERE r.paid_at < r.due_at + interval '48 hours' ) AS renewed FROM bounds b LEFT JOIN renewals r ON r.due_at >= b.starts_at AND r.due_at < b.ends_at WHERE b.ends_at + interval '48 hours' <= timestamptz '2026-11-11 00:00+00' GROUP BY b.cohort_date ) SELECT cohort_date, due, renewed, round(100.0 * renewed / nullif(due, 0), 1) AS renewal_pct FROM counts ORDER BY cohort_date;
Плановая дата продления | Подписок к продлению | Продлили | Доля |
|---|---|---|---|
7 ноября | 3 | 2 | 66,7% |
8 ноября | 2 | 1 | 50,0% |
За 7 ноября засчитались предоплата по записи 101 и оплата после окончания периода по записи 103. Платёж по записи 102 прошёл ровно через 48 часов и оказался вне окна. За 8 ноября платёж по записи 105 прошёл за час до границы. Если в закрытом дне вообще не было подписок к продлению, LEFT JOIN сохранит этот день, а доля будет NULL. В отчёте такую строку можно подписать «не было подписок к продлению».
В рабочем отчёте границу полноты лучше брать из загрузки данных. Если сейчас 11 ноября, но платежи загружены только до полудня 10 ноября, 8 ноября ещё рано выводить как завершённый день.
Сравнивайте дни, когда у всех подписок в группе уже прошло 48 часов с планового срока продления и данные об оплатах за это время загружены. Рядом с процентом показывайте, сколько подписок продлили и сколько всего ждали продления. Так 50% из двух подписок легко отличить от 50% из двух тысяч.
