stripe: Move `make_end_of_cycle_updates_if_needed` to BillingSession.

Moves the 'make_end_of_cycle_updates_if_needed' function to
the 'BillingSession' abstract class.

This refactoring will help in minimizing duplicate code while
supporting both realm and remote_server customers.

Since the function is called from our main daily billing cron job
as well, we have changed 'RealmBillingSession' to accept 'user=None'
(our convention for automated system jobs).
This commit is contained in:
Prakhar Pratyush 2023-11-13 19:35:56 +05:30 committed by Tim Abbott
parent 5accf36115
commit f8a0035215
4 changed files with 219 additions and 185 deletions

View File

@ -54,10 +54,10 @@ if settings.ZILENCER_ENABLED:
if settings.BILLING_ENABLED:
from corporate.lib.stripe import (
RealmBillingSession,
downgrade_at_the_end_of_billing_cycle,
downgrade_now_without_creating_additional_invoices,
get_latest_seat_count,
make_end_of_cycle_updates_if_needed,
switch_realm_from_standard_to_plus_plan,
void_all_open_invoices,
)
@ -185,6 +185,8 @@ def support(
context["success_message"] = request.session["success_message"]
del request.session["success_message"]
acting_user = request.user
assert isinstance(acting_user, UserProfile)
if settings.BILLING_ENABLED and request.method == "POST":
# We check that request.POST only has two keys in it: The
# realm_id and a field to change.
@ -197,8 +199,6 @@ def support(
assert realm_id is not None
realm = Realm.objects.get(id=realm_id)
acting_user = request.user
assert isinstance(acting_user, UserProfile)
if plan_type is not None:
current_plan_type = realm.plan_type
do_change_realm_plan_type(realm, plan_type, acting_user=acting_user)
@ -375,7 +375,8 @@ def support(
current_plan=current_plan,
)
if current_plan is not None:
new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(
billing_session = RealmBillingSession(user=None, realm=realm)
new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(
current_plan, timezone_now()
)
if last_ledger_entry is not None:

View File

@ -255,7 +255,11 @@ def next_invoice_date(plan: CustomerPlan) -> Optional[datetime]:
def renewal_amount(plan: CustomerPlan, event_time: datetime) -> int: # nocoverage: TODO
if plan.fixed_price is not None:
return plan.fixed_price
new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, event_time)
realm = plan.customer.realm
billing_session = RealmBillingSession(user=None, realm=realm)
new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(
plan, event_time
)
if last_ledger_entry is None:
return 0
if last_ledger_entry.licenses_at_next_renewal is None:
@ -402,6 +406,7 @@ class AuditLogEventType(Enum):
SPONSORSHIP_APPROVED = 5
SPONSORSHIP_PENDING_STATUS_CHANGED = 6
BILLING_METHOD_CHANGED = 7
CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 8
class BillingSessionAuditLogEventError(Exception):
@ -464,6 +469,10 @@ class BillingSession(ABC):
def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False) -> None:
pass
@abstractmethod
def process_downgrade(self, plan: CustomerPlan) -> None:
pass
@abstractmethod
def approve_sponsorship(self) -> None:
pass
@ -821,207 +830,11 @@ class BillingSession(ABC):
)
return data
class RealmBillingSession(BillingSession):
def __init__(self, user: UserProfile, realm: Optional[Realm] = None) -> None:
self.user = user
if realm is not None:
assert user.is_staff
self.realm = realm
self.support_session = True
else:
self.realm = user.realm
self.support_session = False
@override
@property
def billing_session_url(self) -> str:
return self.realm.uri
@override
def get_customer(self) -> Optional[Customer]:
return get_customer_by_realm(self.realm)
@override
def current_count_for_billed_licenses(self) -> int:
return get_latest_seat_count(self.realm)
@override
def get_audit_log_event(self, event_type: AuditLogEventType) -> int:
if event_type is AuditLogEventType.STRIPE_CUSTOMER_CREATED:
return RealmAuditLog.STRIPE_CUSTOMER_CREATED
elif event_type is AuditLogEventType.STRIPE_CARD_CHANGED:
return RealmAuditLog.STRIPE_CARD_CHANGED
elif event_type is AuditLogEventType.CUSTOMER_PLAN_CREATED:
return RealmAuditLog.CUSTOMER_PLAN_CREATED
elif event_type is AuditLogEventType.DISCOUNT_CHANGED:
return RealmAuditLog.REALM_DISCOUNT_CHANGED
elif event_type is AuditLogEventType.SPONSORSHIP_APPROVED:
return RealmAuditLog.REALM_SPONSORSHIP_APPROVED
elif event_type is AuditLogEventType.SPONSORSHIP_PENDING_STATUS_CHANGED:
return RealmAuditLog.REALM_SPONSORSHIP_PENDING_STATUS_CHANGED
elif event_type is AuditLogEventType.BILLING_METHOD_CHANGED:
return RealmAuditLog.REALM_BILLING_METHOD_CHANGED
else:
raise BillingSessionAuditLogEventError(event_type)
@override
def write_to_audit_log(
self,
event_type: AuditLogEventType,
event_time: datetime,
*,
extra_data: Optional[Dict[str, Any]] = None,
) -> None:
audit_log_event = self.get_audit_log_event(event_type)
if extra_data:
RealmAuditLog.objects.create(
realm=self.realm,
acting_user=self.user,
event_type=audit_log_event,
event_time=event_time,
extra_data=extra_data,
)
else:
RealmAuditLog.objects.create(
realm=self.realm,
acting_user=self.user,
event_type=audit_log_event,
event_time=event_time,
)
@override
def get_data_for_stripe_customer(self) -> StripeCustomerData:
# Support requests do not set any stripe billing information.
assert self.support_session is False
metadata: Dict[str, Any] = {}
metadata["realm_id"] = self.realm.id
metadata["realm_str"] = self.realm.string_id
realm_stripe_customer_data = StripeCustomerData(
description=f"{self.realm.string_id} ({self.realm.name})",
email=self.user.delivery_email,
metadata=metadata,
)
return realm_stripe_customer_data
@override
def update_data_for_checkout_session_and_payment_intent(
self, metadata: Dict[str, Any]
) -> Dict[str, Any]:
updated_metadata = dict(
user_email=self.user.delivery_email,
realm_id=self.realm.id,
realm_str=self.realm.string_id,
user_id=self.user.id,
**metadata,
)
return updated_metadata
@override
def get_data_for_stripe_payment_intent(
self, price_per_license: int, licenses: int
) -> StripePaymentIntentData:
# Support requests do not set any stripe billing information.
assert self.support_session is False
amount = price_per_license * licenses
description = f"Upgrade to Zulip Cloud Standard, ${price_per_license/100} x {licenses}"
plan_name = "Zulip Cloud Standard"
return StripePaymentIntentData(
amount=amount,
description=description,
plan_name=plan_name,
email=self.user.delivery_email,
)
@override
def update_or_create_customer(
self, stripe_customer_id: Optional[str] = None, *, defaults: Optional[Dict[str, Any]] = None
) -> Customer:
if stripe_customer_id is not None:
# Support requests do not set any stripe billing information.
assert self.support_session is False
customer, created = Customer.objects.update_or_create(
realm=self.realm, defaults={"stripe_customer_id": stripe_customer_id}
)
from zerver.actions.users import do_make_user_billing_admin
do_make_user_billing_admin(self.user)
return customer
else:
customer, created = Customer.objects.update_or_create(
realm=self.realm, defaults=defaults
)
return customer
@override
def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False) -> None:
from zerver.actions.realm_settings import do_change_realm_plan_type
# This function needs to translate between the different
# formats of CustomerPlan.tier and Realm.plan_type.
if is_sponsored:
plan_type = Realm.PLAN_TYPE_STANDARD_FREE
elif tier == CustomerPlan.STANDARD:
plan_type = Realm.PLAN_TYPE_STANDARD
elif tier == CustomerPlan.PLUS: # nocoverage # Plus plan doesn't use this code path yet.
plan_type = Realm.PLAN_TYPE_PLUS
else:
raise AssertionError("Unexpected tier")
do_change_realm_plan_type(self.realm, plan_type, acting_user=self.user)
@override
def approve_sponsorship(self) -> None:
# Sponsorship approval is only a support admin action.
assert self.support_session
from zerver.actions.message_send import internal_send_private_message
self.do_change_plan_type(tier=None, is_sponsored=True)
customer = self.get_customer()
if customer is not None and customer.sponsorship_pending:
customer.sponsorship_pending = False
customer.save(update_fields=["sponsorship_pending"])
self.write_to_audit_log(
event_type=AuditLogEventType.SPONSORSHIP_APPROVED, event_time=timezone_now()
)
notification_bot = get_system_bot(settings.NOTIFICATION_BOT, self.realm.id)
for user in self.realm.get_human_billing_admin_and_realm_owner_users():
with override_language(user.default_language):
# Using variable to make life easier for translators if these details change.
message = _(
"Your organization's request for sponsored hosting has been approved! "
"You have been upgraded to {plan_name}, free of charge. {emoji}\n\n"
"If you could {begin_link}list Zulip as a sponsor on your website{end_link}, "
"we would really appreciate it!"
).format(
plan_name="Zulip Cloud Standard",
emoji=":tada:",
begin_link="[",
end_link="](/help/linking-to-zulip-website)",
)
internal_send_private_message(notification_bot, user, message)
def stripe_customer_has_credit_card_as_default_payment_method(
stripe_customer: stripe.Customer,
) -> bool:
if not stripe_customer.invoice_settings.default_payment_method:
return False
return stripe_customer.invoice_settings.default_payment_method.type == "card"
def customer_has_credit_card_as_default_payment_method(customer: Customer) -> bool:
if not customer.stripe_customer_id:
return False
stripe_customer = stripe_get_customer(customer.stripe_customer_id)
return stripe_customer_has_credit_card_as_default_payment_method(stripe_customer)
# event_time should roughly be timezone_now(). Not designed to handle
# event_times in the past or future
@transaction.atomic
def make_end_of_cycle_updates_if_needed(
plan: CustomerPlan, event_time: datetime
self, plan: CustomerPlan, event_time: datetime
) -> Tuple[Optional[CustomerPlan], Optional[LicenseLedger]]:
last_ledger_entry = LicenseLedger.objects.filter(plan=plan).order_by("-id").first()
last_ledger_renewal = (
@ -1097,13 +910,9 @@ def make_end_of_cycle_updates_if_needed(
licenses_at_next_renewal=licenses_at_next_renewal,
)
realm = new_plan.customer.realm
assert realm is not None
RealmAuditLog.objects.create(
realm=realm,
self.write_to_audit_log(
event_type=AuditLogEventType.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN,
event_time=event_time,
event_type=RealmAuditLog.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN,
extra_data={
"monthly_plan_id": plan.id,
"annual_plan_id": new_plan.id,
@ -1155,11 +964,224 @@ def make_end_of_cycle_updates_if_needed(
return plus_plan, plus_plan_ledger_entry
if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
process_downgrade(plan)
self.process_downgrade(plan)
return None, None
return None, last_ledger_entry
class RealmBillingSession(BillingSession):
def __init__(self, user: Optional[UserProfile] = None, realm: Optional[Realm] = None) -> None:
self.user = user
assert user is not None or realm is not None
if user is not None and realm is not None:
assert user.is_staff
self.realm = realm
self.support_session = True
elif user is not None:
self.realm = user.realm
self.support_session = False
else:
assert realm is not None # for mypy
self.realm = realm
self.support_session = False
@override
@property
def billing_session_url(self) -> str:
return self.realm.uri
@override
def get_customer(self) -> Optional[Customer]:
return get_customer_by_realm(self.realm)
@override
def current_count_for_billed_licenses(self) -> int:
return get_latest_seat_count(self.realm)
@override
def get_audit_log_event(self, event_type: AuditLogEventType) -> int:
if event_type is AuditLogEventType.STRIPE_CUSTOMER_CREATED:
return RealmAuditLog.STRIPE_CUSTOMER_CREATED
elif event_type is AuditLogEventType.STRIPE_CARD_CHANGED:
return RealmAuditLog.STRIPE_CARD_CHANGED
elif event_type is AuditLogEventType.CUSTOMER_PLAN_CREATED:
return RealmAuditLog.CUSTOMER_PLAN_CREATED
elif event_type is AuditLogEventType.DISCOUNT_CHANGED:
return RealmAuditLog.REALM_DISCOUNT_CHANGED
elif event_type is AuditLogEventType.SPONSORSHIP_APPROVED:
return RealmAuditLog.REALM_SPONSORSHIP_APPROVED
elif event_type is AuditLogEventType.SPONSORSHIP_PENDING_STATUS_CHANGED:
return RealmAuditLog.REALM_SPONSORSHIP_PENDING_STATUS_CHANGED
elif event_type is AuditLogEventType.BILLING_METHOD_CHANGED:
return RealmAuditLog.REALM_BILLING_METHOD_CHANGED
elif event_type is AuditLogEventType.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN:
return RealmAuditLog.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN
else:
raise BillingSessionAuditLogEventError(event_type)
@override
def write_to_audit_log(
self,
event_type: AuditLogEventType,
event_time: datetime,
*,
extra_data: Optional[Dict[str, Any]] = None,
) -> None:
audit_log_event = self.get_audit_log_event(event_type)
audit_log_data = {
"realm": self.realm,
"event_type": audit_log_event,
"event_time": event_time,
}
if extra_data:
audit_log_data["extra_data"] = extra_data
if self.user is not None:
audit_log_data["acting_user"] = self.user
RealmAuditLog.objects.create(**audit_log_data)
@override
def get_data_for_stripe_customer(self) -> StripeCustomerData:
# Support requests do not set any stripe billing information.
assert self.support_session is False
assert self.user is not None
metadata: Dict[str, Any] = {}
metadata["realm_id"] = self.realm.id
metadata["realm_str"] = self.realm.string_id
realm_stripe_customer_data = StripeCustomerData(
description=f"{self.realm.string_id} ({self.realm.name})",
email=self.user.delivery_email,
metadata=metadata,
)
return realm_stripe_customer_data
@override
def update_data_for_checkout_session_and_payment_intent(
self, metadata: Dict[str, Any]
) -> Dict[str, Any]:
assert self.user is not None
updated_metadata = dict(
user_email=self.user.delivery_email,
realm_id=self.realm.id,
realm_str=self.realm.string_id,
user_id=self.user.id,
**metadata,
)
return updated_metadata
@override
def get_data_for_stripe_payment_intent(
self, price_per_license: int, licenses: int
) -> StripePaymentIntentData:
# Support requests do not set any stripe billing information.
assert self.support_session is False
assert self.user is not None
amount = price_per_license * licenses
description = f"Upgrade to Zulip Cloud Standard, ${price_per_license/100} x {licenses}"
plan_name = "Zulip Cloud Standard"
return StripePaymentIntentData(
amount=amount,
description=description,
plan_name=plan_name,
email=self.user.delivery_email,
)
@override
def update_or_create_customer(
self, stripe_customer_id: Optional[str] = None, *, defaults: Optional[Dict[str, Any]] = None
) -> Customer:
if stripe_customer_id is not None:
# Support requests do not set any stripe billing information.
assert self.support_session is False
customer, created = Customer.objects.update_or_create(
realm=self.realm, defaults={"stripe_customer_id": stripe_customer_id}
)
from zerver.actions.users import do_make_user_billing_admin
assert self.user is not None
do_make_user_billing_admin(self.user)
return customer
else:
customer, created = Customer.objects.update_or_create(
realm=self.realm, defaults=defaults
)
return customer
@override
def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False) -> None:
from zerver.actions.realm_settings import do_change_realm_plan_type
# This function needs to translate between the different
# formats of CustomerPlan.tier and Realm.plan_type.
if is_sponsored:
plan_type = Realm.PLAN_TYPE_STANDARD_FREE
elif tier == CustomerPlan.STANDARD:
plan_type = Realm.PLAN_TYPE_STANDARD
elif tier == CustomerPlan.PLUS: # nocoverage # Plus plan doesn't use this code path yet.
plan_type = Realm.PLAN_TYPE_PLUS
else:
raise AssertionError("Unexpected tier")
do_change_realm_plan_type(self.realm, plan_type, acting_user=self.user)
@override
def process_downgrade(self, plan: CustomerPlan) -> None:
from zerver.actions.realm_settings import do_change_realm_plan_type
assert plan.customer.realm is not None
do_change_realm_plan_type(plan.customer.realm, Realm.PLAN_TYPE_LIMITED, acting_user=None)
plan.status = CustomerPlan.ENDED
plan.save(update_fields=["status"])
@override
def approve_sponsorship(self) -> None:
# Sponsorship approval is only a support admin action.
assert self.support_session
from zerver.actions.message_send import internal_send_private_message
self.do_change_plan_type(tier=None, is_sponsored=True)
customer = self.get_customer()
if customer is not None and customer.sponsorship_pending:
customer.sponsorship_pending = False
customer.save(update_fields=["sponsorship_pending"])
self.write_to_audit_log(
event_type=AuditLogEventType.SPONSORSHIP_APPROVED, event_time=timezone_now()
)
notification_bot = get_system_bot(settings.NOTIFICATION_BOT, self.realm.id)
for user in self.realm.get_human_billing_admin_and_realm_owner_users():
with override_language(user.default_language):
# Using variable to make life easier for translators if these details change.
message = _(
"Your organization's request for sponsored hosting has been approved! "
"You have been upgraded to {plan_name}, free of charge. {emoji}\n\n"
"If you could {begin_link}list Zulip as a sponsor on your website{end_link}, "
"we would really appreciate it!"
).format(
plan_name="Zulip Cloud Standard",
emoji=":tada:",
begin_link="[",
end_link="](/help/linking-to-zulip-website)",
)
internal_send_private_message(notification_bot, user, message)
def stripe_customer_has_credit_card_as_default_payment_method(
stripe_customer: stripe.Customer,
) -> bool:
if not stripe_customer.invoice_settings.default_payment_method:
return False
return stripe_customer.invoice_settings.default_payment_method.type == "card"
def customer_has_credit_card_as_default_payment_method(customer: Customer) -> bool:
if not customer.stripe_customer_id:
return False
stripe_customer = stripe_get_customer(customer.stripe_customer_id)
return stripe_customer_has_credit_card_as_default_payment_method(stripe_customer)
def calculate_discounted_price_per_license(
original_price_per_license: int, discount: Decimal
) -> int:
@ -1303,7 +1325,10 @@ def update_license_ledger_for_manual_plan(
def update_license_ledger_for_automanaged_plan(
realm: Realm, plan: CustomerPlan, event_time: datetime
) -> None:
new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, event_time)
billing_session = RealmBillingSession(user=None, realm=realm)
new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(
plan, event_time
)
if last_ledger_entry is None:
return
if new_plan is not None:
@ -1345,7 +1370,9 @@ def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None:
f"Realm {plan.customer.realm.string_id} has a paid plan without a Stripe customer."
)
make_end_of_cycle_updates_if_needed(plan, event_time)
realm = plan.customer.realm
billing_session = RealmBillingSession(user=None, realm=realm)
billing_session.make_end_of_cycle_updates_if_needed(plan, event_time)
if plan.invoicing_status == CustomerPlan.INITIAL_INVOICE_TO_BE_SENT:
invoiced_through_id = -1
@ -1466,15 +1493,6 @@ def do_change_plan_status(plan: CustomerPlan, status: int) -> None:
)
def process_downgrade(plan: CustomerPlan) -> None:
from zerver.actions.realm_settings import do_change_realm_plan_type
assert plan.customer.realm is not None
do_change_realm_plan_type(plan.customer.realm, Realm.PLAN_TYPE_LIMITED, acting_user=None)
plan.status = CustomerPlan.ENDED
plan.save(update_fields=["status"])
# During realm deactivation we instantly downgrade the plan to Limited.
# Extra users added in the final month are not charged. Also used
# for the cancellation of Free Trial.
@ -1483,7 +1501,8 @@ def downgrade_now_without_creating_additional_invoices(realm: Realm) -> None:
if plan is None:
return
process_downgrade(plan)
billing_session = RealmBillingSession(user=None, realm=realm)
billing_session.process_downgrade(plan)
plan.invoiced_through = LicenseLedger.objects.filter(plan=plan).order_by("id").last()
plan.next_invoice_date = next_invoice_date(plan)
plan.save(update_fields=["invoiced_through", "next_invoice_date"])

View File

@ -62,7 +62,6 @@ from corporate.lib.stripe import (
is_free_trial_offer_enabled,
is_realm_on_free_trial,
is_sponsored_realm,
make_end_of_cycle_updates_if_needed,
next_month,
sign_string,
stripe_customer_has_credit_card_as_default_payment_method,
@ -4503,11 +4502,17 @@ class LicenseLedgerTest(StripeTestCase):
self.assertEqual(LicenseLedger.objects.count(), 1)
plan = CustomerPlan.objects.get()
# Plan hasn't renewed yet
make_end_of_cycle_updates_if_needed(plan, self.next_year - timedelta(days=1))
realm = plan.customer.realm
billing_session = RealmBillingSession(user=None, realm=realm)
billing_session.make_end_of_cycle_updates_if_needed(
plan, self.next_year - timedelta(days=1)
)
self.assertEqual(LicenseLedger.objects.count(), 1)
# Plan needs to renew
# TODO: do_deactivate_user for a user, so that licenses_at_next_renewal != licenses
new_plan, ledger_entry = make_end_of_cycle_updates_if_needed(plan, self.next_year)
new_plan, ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(
plan, self.next_year
)
self.assertIsNone(new_plan)
self.assertEqual(LicenseLedger.objects.count(), 2)
ledger_params = {
@ -4520,7 +4525,9 @@ class LicenseLedgerTest(StripeTestCase):
for key, value in ledger_params.items():
self.assertEqual(getattr(ledger_entry, key), value)
# Plan needs to renew, but we already added the plan_renewal ledger entry
make_end_of_cycle_updates_if_needed(plan, self.next_year + timedelta(days=1))
billing_session.make_end_of_cycle_updates_if_needed(
plan, self.next_year + timedelta(days=1)
)
self.assertEqual(LicenseLedger.objects.count(), 2)
def test_update_license_ledger_if_needed(self) -> None:
@ -4632,7 +4639,8 @@ class LicenseLedgerTest(StripeTestCase):
self.assertEqual(plan.licenses(), self.seat_count + 10)
self.assertEqual(plan.licenses_at_next_renewal(), self.seat_count + 10)
make_end_of_cycle_updates_if_needed(plan, self.next_year)
billing_session = RealmBillingSession(user=None, realm=realm)
billing_session.make_end_of_cycle_updates_if_needed(plan, self.next_year)
self.assertEqual(plan.licenses(), self.seat_count + 10)
ledger_entries = list(

View File

@ -10,13 +10,13 @@ from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _
from corporate.lib.stripe import (
RealmBillingSession,
cents_to_dollar_string,
do_change_plan_status,
downgrade_at_the_end_of_billing_cycle,
downgrade_now_without_creating_additional_invoices,
format_money,
get_latest_seat_count,
make_end_of_cycle_updates_if_needed,
renewal_amount,
start_of_next_billing_cycle,
stripe_get_customer,
@ -159,7 +159,9 @@ def billing_home(
plan = get_current_plan_by_customer(customer)
if plan is not None:
now = timezone_now()
new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, now)
realm = plan.customer.realm
billing_session = RealmBillingSession(user=None, realm=realm)
new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(plan, now)
if last_ledger_entry is not None:
if new_plan is not None: # nocoverage
plan = new_plan
@ -254,7 +256,11 @@ def update_plan(
plan = get_current_plan_by_realm(user.realm)
assert plan is not None # for mypy
new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, timezone_now())
realm = plan.customer.realm
billing_session = RealmBillingSession(user=None, realm=realm)
new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(
plan, timezone_now()
)
if new_plan is not None:
raise JsonableError(
_("Unable to update the plan. The plan has been expired and replaced with a new plan.")