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

Отмена продления и незавершённого платежа

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

В «Пространстве» отмена подписки означает cancel_at_end: scheduler больше не создаёт новые периоды, а текущее entitlement остаётся до through_until. Отмена pending fixture выполняется отдельным локальным действием, возврат рассмотрим следующим уроком. Полный код находится в BillingEndpoints.cs архива; операции при подготовке не запускались.

Подписка меняет расписание

Owner вызывает маршрут отмены для выбранной компании. Сервис открывает транзакцию, повторно проверяет роль и блокирует подписку. Если она active, state становится cancel_at_end. Если уже cancel_at_end, повтор возвращает тот же результат. Это естественная идемпотентность команды состояния.

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

UPDATE subscriptions
SET state='cancel_at_end'
WHERE organization_id=@organization
  AND state='active'
RETURNING state,period_end;

Ожидаемый ответ показывает дату завершения и отсутствие будущего продления. Если интерфейс пишет «доступ прекращён» сразу после этого ответа, он противоречит серверному контракту. Лучше показать «продление отключено, оплаченный доступ до …». Время отображается в понятной локали, но сравнение срока остаётся UTC.

Состояние компании active при этом не меняется. Базовые проекты продолжают быть доступны после окончания Team; платный экспорт ограничится по entitlement. Если продукт хочет переводить всю организацию в read_only после неоплаты, это должно быть отдельное правило. Наш пример не связывает отмену тарифа с уничтожением данных.

Возобновление имеет своё условие

До конца оплаченного периода owner может вернуть cancel_at_end в active. Прежний anchorDay и сохранённая цена остаются теми же. Такое действие возобновляет расписание на будущую границу, но не создаёт немедленную оплату. Повторная команда снова возвращает active без дополнительных платежных объектов.

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

Не оставляйте право resume всем участникам только потому, что оно кажется обратным действием. Возобновление означает согласие на будущую повторную оплату, поэтому в учебном проекте его выполняет owner. Role editor управляет содержимым проектов, а не расписанием финансовых обязательств компании.

Scheduler и отмена блокируют одну строку

Если владелец отменяет до того, как scheduler создал renewal, новый state предотвращает покупку. Если renewal-заказ уже зафиксирован, поле cancel_at_end останавливает только последующие границы. Оно не удаляет заказ и не доказывает отмену существующего платежа у провайдера.

Для интерфейса это означает необходимость показать незавершённую попытку отдельно. Клиент может увидеть «продление отключено» и одновременно «платёж периода ожидает результата». Такое сообщение точнее единственной зелёной отметки «всё отменено». Сверка определит, была ли попытка успешной, а при необходимости продукт предложит возврат.

Удалять pending-платеж из локальной базы нельзя. Внешний объект может завершиться позднее, и webhook уже не найдёт намерение. Ранее сохранённая сумма и providerId нужны для проверки. Отмена создаёт новое состояние либо команду, но не стирает историю неопределённой операции.

Локальный fixture позволяет вручную перевести pending в canceled. После этого GetAsync возвращает авторитетный снимок, а ApplyPayment закрывает попытку без нового entitlement. Нельзя отправить canceled только в браузере и считать, что адаптер изменился. Любое действие должно пройти своё сохранённое основание и общую сверку.

Успешный платеж нельзя отменить названием статуса

Если snapshot уже succeeded, команда fixture cancellation получает конфликт. Денежный успех сохраняется, а для обратного движения используется refund. Это не косметическое различие: возврат имеет собственную сумму, идентификатор и повторяемый ключ. Удалить успешную запись означало бы потерять факт, с которым сверяются финансовые итоги.

В настоящем провайдере возможности отмены зависят от сценария, включая одностадийную или двухстадийную оплату. Поэтому наш интерфейс не обещает универсальное «Cancel любой payment». Адаптер должен описать допустимость конкретного состояния и вернуть понятный отказ. Учебный контракт завершённых fixtures проще и явно ограничен.

Человек также может закрыть страницу подтверждения, не вызывая серверную отмену. Это действие браузера не меняет платеж. Он может остаться pending или завершиться позже по собственному сроку. Именно поэтому return URL и закрытие окна не используются как финансовые события.

Повтор отмены не сокращает оплаченный срок

Для ручного наблюдения оплатите Team fixture, затем дважды вызовите cancel_at_end. Ожидаемо through_until остаётся прежним, новых заказов нет, а state совпадает. После наступления срока платная функция перестаёт работать из-за сравнения времени, а не потому, что второй повтор отмены что-то отнял.

Рассмотрите другой порядок: tick создал pending renewal, затем owner отменил расписание. Существующий paymentId остаётся видимым и подлежит сверке. Если он canceled, новый период не выдаётся. Если succeeded, факт оплаты сохраняется и требует предметного решения о возврате, а не скрытого отката в pending.

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

Ожидающий клиент получает тот же договор

После отмены браузер перечитывает subscription и features. Старое изображение кнопки не должно самостоятельно инициировать новую покупку при refresh. Если интерфейс потерял ответ cancel, повторяет эту же команду состояния. Если потерял ответ resume, также проверяет фактический state. Ни одна из этих операций не заменяется новым checkout без явного выбора человека. Так сетевой retry остаётся восстановлением информации, а не скрытым согласием на ещё одну финансовую попытку.

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

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