Перейти к содержанию

Подписка и границы календарного месяца

Подтверждённая оплата Team должна дать компании понятный срок доступа. Поле is_paid=true не говорит, когда доступ закончится и какой период оплачивает следующее продление. Поэтому подписка хранит интервал, расписание и согласованные условия, а оплаченный заказ сохраняет конкретные границы.

В «Пространстве» Team оплачивается календарными месяцами по UTC. Это правило учебного продукта: оно явно отличается от тридцатидневного периода. Подписка сохраняет исходный день месяца, чтобы дата 31 не превращалась навсегда в 28 после февраля. Полный расчёт в BillingCalendar.cs доступен в архиве, временные сценарии при подготовке не запускались.

Интервал имеет включённое начало и исключённый конец

Доступ действует при period_start <= now и now < period_end. Такое полуоткрытое правило избегает двойного владения граничным моментом. Следующий период начинается ровно там, где закончился предыдущий. В момент period_end старое право уже не действует, если новый период не подтверждён.

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

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

Якорный день переживает короткий месяц

Пусть начало — 31 января в 12:00 UTC. Следующая граница находится в феврале, где дня 31 нет. Мы выбираем последний день месяца, но сохраняем anchorDay равным 31. После февраля следующая граница снова приходится на 31 марта, а не на 28 марта.

Ниже приведено целое определение вычисления следующей границы из BillingCalendar. Полный проект вызывает его для снимка заказа и будущего продления. Длина месяца определяется DateTime.DaysInMonth.

public static DateTimeOffset NextBoundary(DateTimeOffset current, int anchorDay)
{
    var utc = current.ToUniversalTime();
    var first = new DateTime(utc.Year, utc.Month, 1, 0, 0, 0, DateTimeKind.Utc)
        .AddMonths(1);
    var day = Math.Min(anchorDay, DateTime.DaysInMonth(first.Year, first.Month));
    return new DateTimeOffset(first.AddDays(day - 1).Add(utc.TimeOfDay));
}

Високосный год учитывается тем же методом. Для конца января 2028 граница будет 29 февраля, затем 31 марта. Если просто последовательно вызывать AddMonths на уже сдвинутом 28 февраля, исходный якорь теряется. Отдельное поле сохраняет предметное правило, которое не видно из одной текущей даты.

Период в UTC означает, что локальный отображаемый час иногда может измениться при смене offset пользователя. Наш учебный продукт принимает эту границу. Для подписки «каждый месяц в 12:00 местного времени клиента» потребовались бы часовой пояс, правила неоднозначных часов и другой календарный расчёт. Нельзя обещать поддержку этих вариантов одним AddMonths.

Подписка хранит условия продления

После succeeded транзакция создаёт подписку: code и version плана, сумма, валюта, начало, конец, anchorDay и state. Она также сохраняет ссылку на вымышленный способ fixture-method. Номер карты и другие платежные реквизиты в продукт не попадают.

Сохранённая цена нужна, чтобы продление не подхватило новую строку каталога без согласованного изменения условий. План Team v2 может быть доступен новым покупателям, а существующая подписка продолжает v1. Переход на другую цену требует отдельной команды и понятного момента применения. Курс не скрывает такой переход внутри scheduler.

Одна компания имеет одну действующую подписку учебного плана. Первичный ключ subscription включает компанию. Второй первоначальный заказ при уже активной подписке отклоняется предметным правилом, а не создаёт конкурирующие расписания. Покупку нового периода выполняет renewal-команда, которая ссылается на текущую границу.

При повторном применении succeeded подписка не получает ещё один месяц. Период берётся из конкретного заказа, а orderId является основанием entitlement. Если старое уведомление приходит после нового периода, оно не должно вернуть подписку к прошлой границе. Полный код сравнивает интервалы и сохраняет монотонный результат.

Активность расписания и оплаченный доступ

State подписки определяет, планируется ли следующее продление. Through_until entitlement определяет, до какого момента доступ уже оплачен. Эти значения могут временно различаться. Например, cancel_at_end отключает будущую попытку, но текущий период остаётся действующим до своей границы.

Также pending-продление не увеличивает оплаченный доступ. Если период закончился, а новая попытка ещё не succeeded, платная функция прекращается по правилу курса. Возможный grace period был бы отдельной политикой с ясным сроком. Нельзя молча считать любой pending продолжением оплаты только ради удобства интерфейса.

Для ручного рассмотрения возьмите 31 января и вычислите два следующих месяца, сохраняя anchorDay. Ожидаемые даты — конец февраля и 31 марта. Затем повторите для 15 января: границы остаются на 15 числе. Так видно, что перенос в коротком месяце является исключением даты, а не сменой исходного расписания.

Начало периода и конец ожидания различаются

Valid_until заказа ограничивает возможность впервые создать его платежную попытку. Period_end ограничивает оплаченный доступ. Эти моменты имеют разные назначения и не должны храниться в одном поле expires_at. Заказ может утратить актуальность предложения задолго до конца месяца, а уже созданный внешний платеж всё равно потребует сверки. При потере связи после истечения provider key нужно сохранить неопределённость и запросить разбор вместо нового автоматического создания. В полном локальном примере unbound intent после срока отмечается как требующий рассмотрения.

Календарный якорь также не равен дню первоначального уведомления. Если платеж от 31 января подтверждён 1 февраля, повторное чтение snapshot не должно каждый раз пересчитывать весь период от сегодняшней даты. Заказ уже сохранил границы и anchorDay. Для продукта с началом при подтверждении этот момент назначается один раз в атомарном применении, затем становится таким же неизменяемым основанием. Главное свойство обоих договоров — повтор результата не создаёт новый исходный момент и не даёт дополнительные дни за число уведомлений. При самостоятельном изменении правила сначала обновите контракт заказа, а затем календарный расчёт и выдачу права вместе.

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

Оглавление серии