mirror of
https://github.com/zulip/zulip.git
synced 2025-11-02 04:53:36 +00:00
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:
committed by
Tim Abbott
parent
5accf36115
commit
f8a0035215
@@ -54,10 +54,10 @@ if settings.ZILENCER_ENABLED:
|
|||||||
|
|
||||||
if settings.BILLING_ENABLED:
|
if settings.BILLING_ENABLED:
|
||||||
from corporate.lib.stripe import (
|
from corporate.lib.stripe import (
|
||||||
|
RealmBillingSession,
|
||||||
downgrade_at_the_end_of_billing_cycle,
|
downgrade_at_the_end_of_billing_cycle,
|
||||||
downgrade_now_without_creating_additional_invoices,
|
downgrade_now_without_creating_additional_invoices,
|
||||||
get_latest_seat_count,
|
get_latest_seat_count,
|
||||||
make_end_of_cycle_updates_if_needed,
|
|
||||||
switch_realm_from_standard_to_plus_plan,
|
switch_realm_from_standard_to_plus_plan,
|
||||||
void_all_open_invoices,
|
void_all_open_invoices,
|
||||||
)
|
)
|
||||||
@@ -185,6 +185,8 @@ def support(
|
|||||||
context["success_message"] = request.session["success_message"]
|
context["success_message"] = request.session["success_message"]
|
||||||
del 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":
|
if settings.BILLING_ENABLED and request.method == "POST":
|
||||||
# We check that request.POST only has two keys in it: The
|
# We check that request.POST only has two keys in it: The
|
||||||
# realm_id and a field to change.
|
# realm_id and a field to change.
|
||||||
@@ -197,8 +199,6 @@ def support(
|
|||||||
assert realm_id is not None
|
assert realm_id is not None
|
||||||
realm = Realm.objects.get(id=realm_id)
|
realm = Realm.objects.get(id=realm_id)
|
||||||
|
|
||||||
acting_user = request.user
|
|
||||||
assert isinstance(acting_user, UserProfile)
|
|
||||||
if plan_type is not None:
|
if plan_type is not None:
|
||||||
current_plan_type = realm.plan_type
|
current_plan_type = realm.plan_type
|
||||||
do_change_realm_plan_type(realm, plan_type, acting_user=acting_user)
|
do_change_realm_plan_type(realm, plan_type, acting_user=acting_user)
|
||||||
@@ -375,7 +375,8 @@ def support(
|
|||||||
current_plan=current_plan,
|
current_plan=current_plan,
|
||||||
)
|
)
|
||||||
if current_plan is not None:
|
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()
|
current_plan, timezone_now()
|
||||||
)
|
)
|
||||||
if last_ledger_entry is not None:
|
if last_ledger_entry is not None:
|
||||||
|
|||||||
@@ -255,7 +255,11 @@ def next_invoice_date(plan: CustomerPlan) -> Optional[datetime]:
|
|||||||
def renewal_amount(plan: CustomerPlan, event_time: datetime) -> int: # nocoverage: TODO
|
def renewal_amount(plan: CustomerPlan, event_time: datetime) -> int: # nocoverage: TODO
|
||||||
if plan.fixed_price is not None:
|
if plan.fixed_price is not None:
|
||||||
return plan.fixed_price
|
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:
|
if last_ledger_entry is None:
|
||||||
return 0
|
return 0
|
||||||
if last_ledger_entry.licenses_at_next_renewal is None:
|
if last_ledger_entry.licenses_at_next_renewal is None:
|
||||||
@@ -402,6 +406,7 @@ class AuditLogEventType(Enum):
|
|||||||
SPONSORSHIP_APPROVED = 5
|
SPONSORSHIP_APPROVED = 5
|
||||||
SPONSORSHIP_PENDING_STATUS_CHANGED = 6
|
SPONSORSHIP_PENDING_STATUS_CHANGED = 6
|
||||||
BILLING_METHOD_CHANGED = 7
|
BILLING_METHOD_CHANGED = 7
|
||||||
|
CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 8
|
||||||
|
|
||||||
|
|
||||||
class BillingSessionAuditLogEventError(Exception):
|
class BillingSessionAuditLogEventError(Exception):
|
||||||
@@ -464,6 +469,10 @@ class BillingSession(ABC):
|
|||||||
def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False) -> None:
|
def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def process_downgrade(self, plan: CustomerPlan) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def approve_sponsorship(self) -> None:
|
def approve_sponsorship(self) -> None:
|
||||||
pass
|
pass
|
||||||
@@ -821,17 +830,160 @@ class BillingSession(ABC):
|
|||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
# 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(
|
||||||
|
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 = (
|
||||||
|
LicenseLedger.objects.filter(plan=plan, is_renewal=True).order_by("-id").first()
|
||||||
|
)
|
||||||
|
assert last_ledger_renewal is not None
|
||||||
|
last_renewal = last_ledger_renewal.event_time
|
||||||
|
|
||||||
|
if plan.is_free_trial() or plan.status == CustomerPlan.SWITCH_NOW_FROM_STANDARD_TO_PLUS:
|
||||||
|
assert plan.next_invoice_date is not None
|
||||||
|
next_billing_cycle = plan.next_invoice_date
|
||||||
|
else:
|
||||||
|
next_billing_cycle = start_of_next_billing_cycle(plan, last_renewal)
|
||||||
|
if next_billing_cycle <= event_time and last_ledger_entry is not None:
|
||||||
|
licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal
|
||||||
|
assert licenses_at_next_renewal is not None
|
||||||
|
if plan.status == CustomerPlan.ACTIVE:
|
||||||
|
return None, LicenseLedger.objects.create(
|
||||||
|
plan=plan,
|
||||||
|
is_renewal=True,
|
||||||
|
event_time=next_billing_cycle,
|
||||||
|
licenses=licenses_at_next_renewal,
|
||||||
|
licenses_at_next_renewal=licenses_at_next_renewal,
|
||||||
|
)
|
||||||
|
if plan.is_free_trial():
|
||||||
|
plan.invoiced_through = last_ledger_entry
|
||||||
|
plan.billing_cycle_anchor = next_billing_cycle.replace(microsecond=0)
|
||||||
|
plan.status = CustomerPlan.ACTIVE
|
||||||
|
plan.save(update_fields=["invoiced_through", "billing_cycle_anchor", "status"])
|
||||||
|
return None, LicenseLedger.objects.create(
|
||||||
|
plan=plan,
|
||||||
|
is_renewal=True,
|
||||||
|
event_time=next_billing_cycle,
|
||||||
|
licenses=licenses_at_next_renewal,
|
||||||
|
licenses_at_next_renewal=licenses_at_next_renewal,
|
||||||
|
)
|
||||||
|
|
||||||
|
if plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE:
|
||||||
|
if plan.fixed_price is not None: # nocoverage
|
||||||
|
raise NotImplementedError("Can't switch fixed priced monthly plan to annual.")
|
||||||
|
|
||||||
|
plan.status = CustomerPlan.ENDED
|
||||||
|
plan.save(update_fields=["status"])
|
||||||
|
|
||||||
|
discount = plan.customer.default_discount or plan.discount
|
||||||
|
_, _, _, price_per_license = compute_plan_parameters(
|
||||||
|
tier=plan.tier,
|
||||||
|
automanage_licenses=plan.automanage_licenses,
|
||||||
|
billing_schedule=CustomerPlan.ANNUAL,
|
||||||
|
discount=plan.discount,
|
||||||
|
)
|
||||||
|
|
||||||
|
new_plan = CustomerPlan.objects.create(
|
||||||
|
customer=plan.customer,
|
||||||
|
billing_schedule=CustomerPlan.ANNUAL,
|
||||||
|
automanage_licenses=plan.automanage_licenses,
|
||||||
|
charge_automatically=plan.charge_automatically,
|
||||||
|
price_per_license=price_per_license,
|
||||||
|
discount=discount,
|
||||||
|
billing_cycle_anchor=next_billing_cycle,
|
||||||
|
tier=plan.tier,
|
||||||
|
status=CustomerPlan.ACTIVE,
|
||||||
|
next_invoice_date=next_billing_cycle,
|
||||||
|
invoiced_through=None,
|
||||||
|
invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
new_plan_ledger_entry = LicenseLedger.objects.create(
|
||||||
|
plan=new_plan,
|
||||||
|
is_renewal=True,
|
||||||
|
event_time=next_billing_cycle,
|
||||||
|
licenses=licenses_at_next_renewal,
|
||||||
|
licenses_at_next_renewal=licenses_at_next_renewal,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.write_to_audit_log(
|
||||||
|
event_type=AuditLogEventType.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN,
|
||||||
|
event_time=event_time,
|
||||||
|
extra_data={
|
||||||
|
"monthly_plan_id": plan.id,
|
||||||
|
"annual_plan_id": new_plan.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return new_plan, new_plan_ledger_entry
|
||||||
|
|
||||||
|
if plan.status == CustomerPlan.SWITCH_NOW_FROM_STANDARD_TO_PLUS:
|
||||||
|
standard_plan = plan
|
||||||
|
standard_plan.end_date = next_billing_cycle
|
||||||
|
standard_plan.status = CustomerPlan.ENDED
|
||||||
|
standard_plan.save(update_fields=["status", "end_date"])
|
||||||
|
|
||||||
|
(_, _, _, plus_plan_price_per_license) = compute_plan_parameters(
|
||||||
|
CustomerPlan.PLUS,
|
||||||
|
standard_plan.automanage_licenses,
|
||||||
|
standard_plan.billing_schedule,
|
||||||
|
standard_plan.customer.default_discount,
|
||||||
|
)
|
||||||
|
plus_plan_billing_cycle_anchor = standard_plan.end_date.replace(microsecond=0)
|
||||||
|
|
||||||
|
plus_plan = CustomerPlan.objects.create(
|
||||||
|
customer=standard_plan.customer,
|
||||||
|
status=CustomerPlan.ACTIVE,
|
||||||
|
automanage_licenses=standard_plan.automanage_licenses,
|
||||||
|
charge_automatically=standard_plan.charge_automatically,
|
||||||
|
price_per_license=plus_plan_price_per_license,
|
||||||
|
discount=standard_plan.customer.default_discount,
|
||||||
|
billing_schedule=standard_plan.billing_schedule,
|
||||||
|
tier=CustomerPlan.PLUS,
|
||||||
|
billing_cycle_anchor=plus_plan_billing_cycle_anchor,
|
||||||
|
invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT,
|
||||||
|
next_invoice_date=plus_plan_billing_cycle_anchor,
|
||||||
|
)
|
||||||
|
|
||||||
|
standard_plan_last_ledger = (
|
||||||
|
LicenseLedger.objects.filter(plan=standard_plan).order_by("id").last()
|
||||||
|
)
|
||||||
|
assert standard_plan_last_ledger is not None
|
||||||
|
licenses_for_plus_plan = standard_plan_last_ledger.licenses_at_next_renewal
|
||||||
|
assert licenses_for_plus_plan is not None
|
||||||
|
plus_plan_ledger_entry = LicenseLedger.objects.create(
|
||||||
|
plan=plus_plan,
|
||||||
|
is_renewal=True,
|
||||||
|
event_time=plus_plan_billing_cycle_anchor,
|
||||||
|
licenses=licenses_for_plus_plan,
|
||||||
|
licenses_at_next_renewal=licenses_for_plus_plan,
|
||||||
|
)
|
||||||
|
return plus_plan, plus_plan_ledger_entry
|
||||||
|
|
||||||
|
if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
|
||||||
|
self.process_downgrade(plan)
|
||||||
|
return None, None
|
||||||
|
return None, last_ledger_entry
|
||||||
|
|
||||||
|
|
||||||
class RealmBillingSession(BillingSession):
|
class RealmBillingSession(BillingSession):
|
||||||
def __init__(self, user: UserProfile, realm: Optional[Realm] = None) -> None:
|
def __init__(self, user: Optional[UserProfile] = None, realm: Optional[Realm] = None) -> None:
|
||||||
self.user = user
|
self.user = user
|
||||||
if realm is not None:
|
assert user is not None or realm is not None
|
||||||
|
if user is not None and realm is not None:
|
||||||
assert user.is_staff
|
assert user.is_staff
|
||||||
self.realm = realm
|
self.realm = realm
|
||||||
self.support_session = True
|
self.support_session = True
|
||||||
else:
|
elif user is not None:
|
||||||
self.realm = user.realm
|
self.realm = user.realm
|
||||||
self.support_session = False
|
self.support_session = False
|
||||||
|
else:
|
||||||
|
assert realm is not None # for mypy
|
||||||
|
self.realm = realm
|
||||||
|
self.support_session = False
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@property
|
@property
|
||||||
@@ -862,6 +1014,8 @@ class RealmBillingSession(BillingSession):
|
|||||||
return RealmAuditLog.REALM_SPONSORSHIP_PENDING_STATUS_CHANGED
|
return RealmAuditLog.REALM_SPONSORSHIP_PENDING_STATUS_CHANGED
|
||||||
elif event_type is AuditLogEventType.BILLING_METHOD_CHANGED:
|
elif event_type is AuditLogEventType.BILLING_METHOD_CHANGED:
|
||||||
return RealmAuditLog.REALM_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:
|
else:
|
||||||
raise BillingSessionAuditLogEventError(event_type)
|
raise BillingSessionAuditLogEventError(event_type)
|
||||||
|
|
||||||
@@ -874,26 +1028,25 @@ class RealmBillingSession(BillingSession):
|
|||||||
extra_data: Optional[Dict[str, Any]] = None,
|
extra_data: Optional[Dict[str, Any]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
audit_log_event = self.get_audit_log_event(event_type)
|
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:
|
if extra_data:
|
||||||
RealmAuditLog.objects.create(
|
audit_log_data["extra_data"] = extra_data
|
||||||
realm=self.realm,
|
|
||||||
acting_user=self.user,
|
if self.user is not None:
|
||||||
event_type=audit_log_event,
|
audit_log_data["acting_user"] = self.user
|
||||||
event_time=event_time,
|
|
||||||
extra_data=extra_data,
|
RealmAuditLog.objects.create(**audit_log_data)
|
||||||
)
|
|
||||||
else:
|
|
||||||
RealmAuditLog.objects.create(
|
|
||||||
realm=self.realm,
|
|
||||||
acting_user=self.user,
|
|
||||||
event_type=audit_log_event,
|
|
||||||
event_time=event_time,
|
|
||||||
)
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def get_data_for_stripe_customer(self) -> StripeCustomerData:
|
def get_data_for_stripe_customer(self) -> StripeCustomerData:
|
||||||
# Support requests do not set any stripe billing information.
|
# Support requests do not set any stripe billing information.
|
||||||
assert self.support_session is False
|
assert self.support_session is False
|
||||||
|
assert self.user is not None
|
||||||
metadata: Dict[str, Any] = {}
|
metadata: Dict[str, Any] = {}
|
||||||
metadata["realm_id"] = self.realm.id
|
metadata["realm_id"] = self.realm.id
|
||||||
metadata["realm_str"] = self.realm.string_id
|
metadata["realm_str"] = self.realm.string_id
|
||||||
@@ -908,6 +1061,7 @@ class RealmBillingSession(BillingSession):
|
|||||||
def update_data_for_checkout_session_and_payment_intent(
|
def update_data_for_checkout_session_and_payment_intent(
|
||||||
self, metadata: Dict[str, Any]
|
self, metadata: Dict[str, Any]
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
assert self.user is not None
|
||||||
updated_metadata = dict(
|
updated_metadata = dict(
|
||||||
user_email=self.user.delivery_email,
|
user_email=self.user.delivery_email,
|
||||||
realm_id=self.realm.id,
|
realm_id=self.realm.id,
|
||||||
@@ -923,6 +1077,7 @@ class RealmBillingSession(BillingSession):
|
|||||||
) -> StripePaymentIntentData:
|
) -> StripePaymentIntentData:
|
||||||
# Support requests do not set any stripe billing information.
|
# Support requests do not set any stripe billing information.
|
||||||
assert self.support_session is False
|
assert self.support_session is False
|
||||||
|
assert self.user is not None
|
||||||
amount = price_per_license * licenses
|
amount = price_per_license * licenses
|
||||||
description = f"Upgrade to Zulip Cloud Standard, ${price_per_license/100} x {licenses}"
|
description = f"Upgrade to Zulip Cloud Standard, ${price_per_license/100} x {licenses}"
|
||||||
plan_name = "Zulip Cloud Standard"
|
plan_name = "Zulip Cloud Standard"
|
||||||
@@ -945,6 +1100,7 @@ class RealmBillingSession(BillingSession):
|
|||||||
)
|
)
|
||||||
from zerver.actions.users import do_make_user_billing_admin
|
from zerver.actions.users import do_make_user_billing_admin
|
||||||
|
|
||||||
|
assert self.user is not None
|
||||||
do_make_user_billing_admin(self.user)
|
do_make_user_billing_admin(self.user)
|
||||||
return customer
|
return customer
|
||||||
else:
|
else:
|
||||||
@@ -969,6 +1125,15 @@ class RealmBillingSession(BillingSession):
|
|||||||
raise AssertionError("Unexpected tier")
|
raise AssertionError("Unexpected tier")
|
||||||
do_change_realm_plan_type(self.realm, plan_type, acting_user=self.user)
|
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
|
@override
|
||||||
def approve_sponsorship(self) -> None:
|
def approve_sponsorship(self) -> None:
|
||||||
# Sponsorship approval is only a support admin action.
|
# Sponsorship approval is only a support admin action.
|
||||||
@@ -1017,149 +1182,6 @@ def customer_has_credit_card_as_default_payment_method(customer: Customer) -> bo
|
|||||||
return stripe_customer_has_credit_card_as_default_payment_method(stripe_customer)
|
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
|
|
||||||
) -> Tuple[Optional[CustomerPlan], Optional[LicenseLedger]]:
|
|
||||||
last_ledger_entry = LicenseLedger.objects.filter(plan=plan).order_by("-id").first()
|
|
||||||
last_ledger_renewal = (
|
|
||||||
LicenseLedger.objects.filter(plan=plan, is_renewal=True).order_by("-id").first()
|
|
||||||
)
|
|
||||||
assert last_ledger_renewal is not None
|
|
||||||
last_renewal = last_ledger_renewal.event_time
|
|
||||||
|
|
||||||
if plan.is_free_trial() or plan.status == CustomerPlan.SWITCH_NOW_FROM_STANDARD_TO_PLUS:
|
|
||||||
assert plan.next_invoice_date is not None
|
|
||||||
next_billing_cycle = plan.next_invoice_date
|
|
||||||
else:
|
|
||||||
next_billing_cycle = start_of_next_billing_cycle(plan, last_renewal)
|
|
||||||
if next_billing_cycle <= event_time and last_ledger_entry is not None:
|
|
||||||
licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal
|
|
||||||
assert licenses_at_next_renewal is not None
|
|
||||||
if plan.status == CustomerPlan.ACTIVE:
|
|
||||||
return None, LicenseLedger.objects.create(
|
|
||||||
plan=plan,
|
|
||||||
is_renewal=True,
|
|
||||||
event_time=next_billing_cycle,
|
|
||||||
licenses=licenses_at_next_renewal,
|
|
||||||
licenses_at_next_renewal=licenses_at_next_renewal,
|
|
||||||
)
|
|
||||||
if plan.is_free_trial():
|
|
||||||
plan.invoiced_through = last_ledger_entry
|
|
||||||
plan.billing_cycle_anchor = next_billing_cycle.replace(microsecond=0)
|
|
||||||
plan.status = CustomerPlan.ACTIVE
|
|
||||||
plan.save(update_fields=["invoiced_through", "billing_cycle_anchor", "status"])
|
|
||||||
return None, LicenseLedger.objects.create(
|
|
||||||
plan=plan,
|
|
||||||
is_renewal=True,
|
|
||||||
event_time=next_billing_cycle,
|
|
||||||
licenses=licenses_at_next_renewal,
|
|
||||||
licenses_at_next_renewal=licenses_at_next_renewal,
|
|
||||||
)
|
|
||||||
|
|
||||||
if plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE:
|
|
||||||
if plan.fixed_price is not None: # nocoverage
|
|
||||||
raise NotImplementedError("Can't switch fixed priced monthly plan to annual.")
|
|
||||||
|
|
||||||
plan.status = CustomerPlan.ENDED
|
|
||||||
plan.save(update_fields=["status"])
|
|
||||||
|
|
||||||
discount = plan.customer.default_discount or plan.discount
|
|
||||||
_, _, _, price_per_license = compute_plan_parameters(
|
|
||||||
tier=plan.tier,
|
|
||||||
automanage_licenses=plan.automanage_licenses,
|
|
||||||
billing_schedule=CustomerPlan.ANNUAL,
|
|
||||||
discount=plan.discount,
|
|
||||||
)
|
|
||||||
|
|
||||||
new_plan = CustomerPlan.objects.create(
|
|
||||||
customer=plan.customer,
|
|
||||||
billing_schedule=CustomerPlan.ANNUAL,
|
|
||||||
automanage_licenses=plan.automanage_licenses,
|
|
||||||
charge_automatically=plan.charge_automatically,
|
|
||||||
price_per_license=price_per_license,
|
|
||||||
discount=discount,
|
|
||||||
billing_cycle_anchor=next_billing_cycle,
|
|
||||||
tier=plan.tier,
|
|
||||||
status=CustomerPlan.ACTIVE,
|
|
||||||
next_invoice_date=next_billing_cycle,
|
|
||||||
invoiced_through=None,
|
|
||||||
invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT,
|
|
||||||
)
|
|
||||||
|
|
||||||
new_plan_ledger_entry = LicenseLedger.objects.create(
|
|
||||||
plan=new_plan,
|
|
||||||
is_renewal=True,
|
|
||||||
event_time=next_billing_cycle,
|
|
||||||
licenses=licenses_at_next_renewal,
|
|
||||||
licenses_at_next_renewal=licenses_at_next_renewal,
|
|
||||||
)
|
|
||||||
|
|
||||||
realm = new_plan.customer.realm
|
|
||||||
assert realm is not None
|
|
||||||
|
|
||||||
RealmAuditLog.objects.create(
|
|
||||||
realm=realm,
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return new_plan, new_plan_ledger_entry
|
|
||||||
|
|
||||||
if plan.status == CustomerPlan.SWITCH_NOW_FROM_STANDARD_TO_PLUS:
|
|
||||||
standard_plan = plan
|
|
||||||
standard_plan.end_date = next_billing_cycle
|
|
||||||
standard_plan.status = CustomerPlan.ENDED
|
|
||||||
standard_plan.save(update_fields=["status", "end_date"])
|
|
||||||
|
|
||||||
(_, _, _, plus_plan_price_per_license) = compute_plan_parameters(
|
|
||||||
CustomerPlan.PLUS,
|
|
||||||
standard_plan.automanage_licenses,
|
|
||||||
standard_plan.billing_schedule,
|
|
||||||
standard_plan.customer.default_discount,
|
|
||||||
)
|
|
||||||
plus_plan_billing_cycle_anchor = standard_plan.end_date.replace(microsecond=0)
|
|
||||||
|
|
||||||
plus_plan = CustomerPlan.objects.create(
|
|
||||||
customer=standard_plan.customer,
|
|
||||||
status=CustomerPlan.ACTIVE,
|
|
||||||
automanage_licenses=standard_plan.automanage_licenses,
|
|
||||||
charge_automatically=standard_plan.charge_automatically,
|
|
||||||
price_per_license=plus_plan_price_per_license,
|
|
||||||
discount=standard_plan.customer.default_discount,
|
|
||||||
billing_schedule=standard_plan.billing_schedule,
|
|
||||||
tier=CustomerPlan.PLUS,
|
|
||||||
billing_cycle_anchor=plus_plan_billing_cycle_anchor,
|
|
||||||
invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT,
|
|
||||||
next_invoice_date=plus_plan_billing_cycle_anchor,
|
|
||||||
)
|
|
||||||
|
|
||||||
standard_plan_last_ledger = (
|
|
||||||
LicenseLedger.objects.filter(plan=standard_plan).order_by("id").last()
|
|
||||||
)
|
|
||||||
assert standard_plan_last_ledger is not None
|
|
||||||
licenses_for_plus_plan = standard_plan_last_ledger.licenses_at_next_renewal
|
|
||||||
assert licenses_for_plus_plan is not None
|
|
||||||
plus_plan_ledger_entry = LicenseLedger.objects.create(
|
|
||||||
plan=plus_plan,
|
|
||||||
is_renewal=True,
|
|
||||||
event_time=plus_plan_billing_cycle_anchor,
|
|
||||||
licenses=licenses_for_plus_plan,
|
|
||||||
licenses_at_next_renewal=licenses_for_plus_plan,
|
|
||||||
)
|
|
||||||
return plus_plan, plus_plan_ledger_entry
|
|
||||||
|
|
||||||
if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
|
|
||||||
process_downgrade(plan)
|
|
||||||
return None, None
|
|
||||||
return None, last_ledger_entry
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_discounted_price_per_license(
|
def calculate_discounted_price_per_license(
|
||||||
original_price_per_license: int, discount: Decimal
|
original_price_per_license: int, discount: Decimal
|
||||||
) -> int:
|
) -> int:
|
||||||
@@ -1303,7 +1325,10 @@ def update_license_ledger_for_manual_plan(
|
|||||||
def update_license_ledger_for_automanaged_plan(
|
def update_license_ledger_for_automanaged_plan(
|
||||||
realm: Realm, plan: CustomerPlan, event_time: datetime
|
realm: Realm, plan: CustomerPlan, event_time: datetime
|
||||||
) -> None:
|
) -> 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:
|
if last_ledger_entry is None:
|
||||||
return
|
return
|
||||||
if new_plan is not None:
|
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."
|
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:
|
if plan.invoicing_status == CustomerPlan.INITIAL_INVOICE_TO_BE_SENT:
|
||||||
invoiced_through_id = -1
|
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.
|
# During realm deactivation we instantly downgrade the plan to Limited.
|
||||||
# Extra users added in the final month are not charged. Also used
|
# Extra users added in the final month are not charged. Also used
|
||||||
# for the cancellation of Free Trial.
|
# for the cancellation of Free Trial.
|
||||||
@@ -1483,7 +1501,8 @@ def downgrade_now_without_creating_additional_invoices(realm: Realm) -> None:
|
|||||||
if plan is None:
|
if plan is None:
|
||||||
return
|
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.invoiced_through = LicenseLedger.objects.filter(plan=plan).order_by("id").last()
|
||||||
plan.next_invoice_date = next_invoice_date(plan)
|
plan.next_invoice_date = next_invoice_date(plan)
|
||||||
plan.save(update_fields=["invoiced_through", "next_invoice_date"])
|
plan.save(update_fields=["invoiced_through", "next_invoice_date"])
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ from corporate.lib.stripe import (
|
|||||||
is_free_trial_offer_enabled,
|
is_free_trial_offer_enabled,
|
||||||
is_realm_on_free_trial,
|
is_realm_on_free_trial,
|
||||||
is_sponsored_realm,
|
is_sponsored_realm,
|
||||||
make_end_of_cycle_updates_if_needed,
|
|
||||||
next_month,
|
next_month,
|
||||||
sign_string,
|
sign_string,
|
||||||
stripe_customer_has_credit_card_as_default_payment_method,
|
stripe_customer_has_credit_card_as_default_payment_method,
|
||||||
@@ -4503,11 +4502,17 @@ class LicenseLedgerTest(StripeTestCase):
|
|||||||
self.assertEqual(LicenseLedger.objects.count(), 1)
|
self.assertEqual(LicenseLedger.objects.count(), 1)
|
||||||
plan = CustomerPlan.objects.get()
|
plan = CustomerPlan.objects.get()
|
||||||
# Plan hasn't renewed yet
|
# 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)
|
self.assertEqual(LicenseLedger.objects.count(), 1)
|
||||||
# Plan needs to renew
|
# Plan needs to renew
|
||||||
# TODO: do_deactivate_user for a user, so that licenses_at_next_renewal != licenses
|
# 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.assertIsNone(new_plan)
|
||||||
self.assertEqual(LicenseLedger.objects.count(), 2)
|
self.assertEqual(LicenseLedger.objects.count(), 2)
|
||||||
ledger_params = {
|
ledger_params = {
|
||||||
@@ -4520,7 +4525,9 @@ class LicenseLedgerTest(StripeTestCase):
|
|||||||
for key, value in ledger_params.items():
|
for key, value in ledger_params.items():
|
||||||
self.assertEqual(getattr(ledger_entry, key), value)
|
self.assertEqual(getattr(ledger_entry, key), value)
|
||||||
# Plan needs to renew, but we already added the plan_renewal ledger entry
|
# 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)
|
self.assertEqual(LicenseLedger.objects.count(), 2)
|
||||||
|
|
||||||
def test_update_license_ledger_if_needed(self) -> None:
|
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(), self.seat_count + 10)
|
||||||
self.assertEqual(plan.licenses_at_next_renewal(), 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)
|
self.assertEqual(plan.licenses(), self.seat_count + 10)
|
||||||
|
|
||||||
ledger_entries = list(
|
ledger_entries = list(
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ from django.utils.timezone import now as timezone_now
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from corporate.lib.stripe import (
|
from corporate.lib.stripe import (
|
||||||
|
RealmBillingSession,
|
||||||
cents_to_dollar_string,
|
cents_to_dollar_string,
|
||||||
do_change_plan_status,
|
do_change_plan_status,
|
||||||
downgrade_at_the_end_of_billing_cycle,
|
downgrade_at_the_end_of_billing_cycle,
|
||||||
downgrade_now_without_creating_additional_invoices,
|
downgrade_now_without_creating_additional_invoices,
|
||||||
format_money,
|
format_money,
|
||||||
get_latest_seat_count,
|
get_latest_seat_count,
|
||||||
make_end_of_cycle_updates_if_needed,
|
|
||||||
renewal_amount,
|
renewal_amount,
|
||||||
start_of_next_billing_cycle,
|
start_of_next_billing_cycle,
|
||||||
stripe_get_customer,
|
stripe_get_customer,
|
||||||
@@ -159,7 +159,9 @@ def billing_home(
|
|||||||
plan = get_current_plan_by_customer(customer)
|
plan = get_current_plan_by_customer(customer)
|
||||||
if plan is not None:
|
if plan is not None:
|
||||||
now = timezone_now()
|
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 last_ledger_entry is not None:
|
||||||
if new_plan is not None: # nocoverage
|
if new_plan is not None: # nocoverage
|
||||||
plan = new_plan
|
plan = new_plan
|
||||||
@@ -254,7 +256,11 @@ def update_plan(
|
|||||||
plan = get_current_plan_by_realm(user.realm)
|
plan = get_current_plan_by_realm(user.realm)
|
||||||
assert plan is not None # for mypy
|
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:
|
if new_plan is not None:
|
||||||
raise JsonableError(
|
raise JsonableError(
|
||||||
_("Unable to update the plan. The plan has been expired and replaced with a new plan.")
|
_("Unable to update the plan. The plan has been expired and replaced with a new plan.")
|
||||||
|
|||||||
Reference in New Issue
Block a user