mirror of https://github.com/zulip/zulip.git
stripe: Add 'do_update_plan' method to the 'BillingSession' class.
This commit moves a major portion of the 'update_plan` view to a new shared 'BillingSession.do_update_plan' method. This refactoring will help in minimizing duplicate code while supporting both realm and remote_server customers.
This commit is contained in:
parent
efa423395f
commit
51b39cb682
|
@ -701,7 +701,9 @@ class TestSupportEndpoint(ZulipTestCase):
|
|||
iago = self.example_user("iago")
|
||||
self.login_user(iago)
|
||||
|
||||
with mock.patch("analytics.views.support.downgrade_at_the_end_of_billing_cycle") as m:
|
||||
with mock.patch(
|
||||
"analytics.views.support.RealmBillingSession.downgrade_at_the_end_of_billing_cycle"
|
||||
) as m:
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{
|
||||
|
@ -709,13 +711,13 @@ class TestSupportEndpoint(ZulipTestCase):
|
|||
"modify_plan": "downgrade_at_billing_cycle_end",
|
||||
},
|
||||
)
|
||||
m.assert_called_once_with(get_realm("zulip"))
|
||||
m.assert_called_once()
|
||||
self.assert_in_success_response(
|
||||
["zulip marked for downgrade at the end of billing cycle"], result
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"analytics.views.support.downgrade_now_without_creating_additional_invoices"
|
||||
"analytics.views.support.RealmBillingSession.downgrade_now_without_creating_additional_invoices"
|
||||
) as m:
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
|
@ -724,13 +726,13 @@ class TestSupportEndpoint(ZulipTestCase):
|
|||
"modify_plan": "downgrade_now_without_additional_licenses",
|
||||
},
|
||||
)
|
||||
m.assert_called_once_with(get_realm("zulip"))
|
||||
m.assert_called_once()
|
||||
self.assert_in_success_response(
|
||||
["zulip downgraded without creating additional invoices"], result
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"analytics.views.support.downgrade_now_without_creating_additional_invoices"
|
||||
"analytics.views.support.RealmBillingSession.downgrade_now_without_creating_additional_invoices"
|
||||
) as m1:
|
||||
with mock.patch("analytics.views.support.void_all_open_invoices", return_value=1) as m2:
|
||||
result = self.client_post(
|
||||
|
@ -740,7 +742,7 @@ class TestSupportEndpoint(ZulipTestCase):
|
|||
"modify_plan": "downgrade_now_void_open_invoices",
|
||||
},
|
||||
)
|
||||
m1.assert_called_once_with(get_realm("zulip"))
|
||||
m1.assert_called_once()
|
||||
m2.assert_called_once_with(get_realm("zulip"))
|
||||
self.assert_in_success_response(
|
||||
["zulip downgraded and voided 1 open invoices"], result
|
||||
|
|
|
@ -55,8 +55,6 @@ 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,
|
||||
switch_realm_from_standard_to_plus_plan,
|
||||
void_all_open_invoices,
|
||||
|
@ -264,18 +262,21 @@ def support(
|
|||
approve_realm_sponsorship(realm, acting_user=acting_user)
|
||||
context["success_message"] = f"Sponsorship approved for {realm.string_id}"
|
||||
elif modify_plan is not None:
|
||||
billing_session = RealmBillingSession(
|
||||
user=acting_user, realm=realm, support_session=True
|
||||
)
|
||||
if modify_plan == "downgrade_at_billing_cycle_end":
|
||||
downgrade_at_the_end_of_billing_cycle(realm)
|
||||
billing_session.downgrade_at_the_end_of_billing_cycle()
|
||||
context[
|
||||
"success_message"
|
||||
] = f"{realm.string_id} marked for downgrade at the end of billing cycle"
|
||||
elif modify_plan == "downgrade_now_without_additional_licenses":
|
||||
downgrade_now_without_creating_additional_invoices(realm)
|
||||
billing_session.downgrade_now_without_creating_additional_invoices()
|
||||
context[
|
||||
"success_message"
|
||||
] = f"{realm.string_id} downgraded without creating additional invoices"
|
||||
elif modify_plan == "downgrade_now_void_open_invoices":
|
||||
downgrade_now_without_creating_additional_invoices(realm)
|
||||
billing_session.downgrade_now_without_creating_additional_invoices()
|
||||
voided_invoices_count = void_all_open_invoices(realm)
|
||||
context[
|
||||
"success_message"
|
||||
|
|
|
@ -451,6 +451,13 @@ class InitialUpgradeRequest:
|
|||
tier: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class UpdatePlanRequest:
|
||||
status: Optional[int]
|
||||
licenses: Optional[int]
|
||||
licenses_at_next_renewal: Optional[int]
|
||||
|
||||
|
||||
class AuditLogEventType(Enum):
|
||||
STRIPE_CUSTOMER_CREATED = 1
|
||||
STRIPE_CARD_CHANGED = 2
|
||||
|
@ -1264,6 +1271,134 @@ class BillingSession(ABC):
|
|||
|
||||
return None, context
|
||||
|
||||
def downgrade_at_the_end_of_billing_cycle(self, plan: Optional[CustomerPlan] = None) -> None:
|
||||
if plan is None: # nocoverage
|
||||
# TODO: Add test coverage. Right now, this logic is used
|
||||
# in production but mocked in tests.
|
||||
customer = self.get_customer()
|
||||
assert customer is not None
|
||||
plan = get_current_plan_by_customer(customer)
|
||||
assert plan is not None
|
||||
do_change_plan_status(plan, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE)
|
||||
|
||||
# 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.
|
||||
def downgrade_now_without_creating_additional_invoices(
|
||||
self,
|
||||
plan: Optional[CustomerPlan] = None,
|
||||
) -> None:
|
||||
if plan is None:
|
||||
customer = self.get_customer()
|
||||
if customer is None:
|
||||
return
|
||||
plan = get_current_plan_by_customer(customer)
|
||||
if plan is None:
|
||||
return # nocoverage
|
||||
|
||||
self.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"])
|
||||
|
||||
def do_update_plan(self, update_plan_request: UpdatePlanRequest) -> None:
|
||||
customer = self.get_customer()
|
||||
assert customer is not None
|
||||
plan = get_current_plan_by_customer(customer)
|
||||
assert plan is not None # for mypy
|
||||
|
||||
new_plan, last_ledger_entry = self.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."
|
||||
)
|
||||
)
|
||||
|
||||
if last_ledger_entry is None:
|
||||
raise JsonableError(_("Unable to update the plan. The plan has ended."))
|
||||
|
||||
status = update_plan_request.status
|
||||
if status is not None:
|
||||
if status == CustomerPlan.ACTIVE:
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
do_change_plan_status(plan, status)
|
||||
elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
self.downgrade_at_the_end_of_billing_cycle(plan=plan)
|
||||
elif status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE:
|
||||
assert plan.billing_schedule == CustomerPlan.MONTHLY
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
# Customer needs to switch to an active plan first to avoid unexpected behavior.
|
||||
assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE
|
||||
assert plan.fixed_price is None
|
||||
do_change_plan_status(plan, status)
|
||||
elif status == CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE:
|
||||
assert plan.billing_schedule == CustomerPlan.ANNUAL
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
# Customer needs to switch to an active plan first to avoid unexpected behavior.
|
||||
assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE
|
||||
assert plan.fixed_price is None
|
||||
do_change_plan_status(plan, status)
|
||||
elif status == CustomerPlan.ENDED:
|
||||
assert plan.is_free_trial()
|
||||
self.downgrade_now_without_creating_additional_invoices(plan=plan)
|
||||
return
|
||||
|
||||
licenses = update_plan_request.licenses
|
||||
if licenses is not None:
|
||||
if plan.automanage_licenses:
|
||||
raise JsonableError(
|
||||
_(
|
||||
"Unable to update licenses manually. Your plan is on automatic license management."
|
||||
)
|
||||
)
|
||||
if last_ledger_entry.licenses == licenses:
|
||||
raise JsonableError(
|
||||
_(
|
||||
"Your plan is already on {licenses} licenses in the current billing period."
|
||||
).format(licenses=licenses)
|
||||
)
|
||||
if last_ledger_entry.licenses > licenses:
|
||||
raise JsonableError(
|
||||
_("You cannot decrease the licenses in the current billing period.")
|
||||
)
|
||||
validate_licenses(
|
||||
plan.charge_automatically,
|
||||
licenses,
|
||||
self.current_count_for_billed_licenses(),
|
||||
plan.customer.exempt_from_license_number_check,
|
||||
)
|
||||
update_license_ledger_for_manual_plan(plan, timezone_now(), licenses=licenses)
|
||||
return
|
||||
|
||||
licenses_at_next_renewal = update_plan_request.licenses_at_next_renewal
|
||||
if licenses_at_next_renewal is not None:
|
||||
if plan.automanage_licenses:
|
||||
raise JsonableError(
|
||||
_(
|
||||
"Unable to update licenses manually. Your plan is on automatic license management."
|
||||
)
|
||||
)
|
||||
if last_ledger_entry.licenses_at_next_renewal == licenses_at_next_renewal:
|
||||
raise JsonableError(
|
||||
_(
|
||||
"Your plan is already scheduled to renew with {licenses_at_next_renewal} licenses."
|
||||
).format(licenses_at_next_renewal=licenses_at_next_renewal)
|
||||
)
|
||||
validate_licenses(
|
||||
plan.charge_automatically,
|
||||
licenses_at_next_renewal,
|
||||
self.current_count_for_billed_licenses(),
|
||||
plan.customer.exempt_from_license_number_check,
|
||||
)
|
||||
update_license_ledger_for_manual_plan(
|
||||
plan, timezone_now(), licenses_at_next_renewal=licenses_at_next_renewal
|
||||
)
|
||||
return
|
||||
|
||||
raise JsonableError(_("Nothing to change."))
|
||||
|
||||
|
||||
class RealmBillingSession(BillingSession):
|
||||
def __init__(
|
||||
|
@ -2174,27 +2309,6 @@ def do_change_plan_status(plan: CustomerPlan, status: int) -> None:
|
|||
)
|
||||
|
||||
|
||||
# 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.
|
||||
def downgrade_now_without_creating_additional_invoices(realm: Realm) -> None:
|
||||
plan = get_current_plan_by_realm(realm)
|
||||
if plan is None:
|
||||
return
|
||||
|
||||
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"])
|
||||
|
||||
|
||||
def downgrade_at_the_end_of_billing_cycle(realm: Realm) -> None:
|
||||
plan = get_current_plan_by_realm(realm)
|
||||
assert plan is not None
|
||||
do_change_plan_status(plan, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE)
|
||||
|
||||
|
||||
def get_all_invoices_for_customer(customer: Customer) -> Generator[stripe.Invoice, None, None]:
|
||||
if customer.stripe_customer_id is None:
|
||||
return
|
||||
|
@ -2251,8 +2365,8 @@ def downgrade_small_realms_behind_on_payments_as_needed() -> None:
|
|||
continue
|
||||
|
||||
# We've now decided to downgrade this customer and void all invoices, and the below will execute this.
|
||||
|
||||
downgrade_now_without_creating_additional_invoices(realm)
|
||||
billing_session = RealmBillingSession(user=None, realm=realm)
|
||||
billing_session.downgrade_now_without_creating_additional_invoices()
|
||||
void_all_open_invoices(realm)
|
||||
context: Dict[str, Union[str, Realm]] = {
|
||||
"upgrade_url": f"{realm.uri}{reverse('initial_upgrade')}",
|
||||
|
|
|
@ -2185,7 +2185,7 @@ class StripeTest(StripeTestCase):
|
|||
self.assertEqual(plan.licenses(), self.seat_count)
|
||||
self.assertEqual(plan.licenses_at_next_renewal(), self.seat_count)
|
||||
with self.assertLogs("corporate.stripe", "INFO") as m:
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
response = self.client_patch(
|
||||
"/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}
|
||||
)
|
||||
|
@ -2299,7 +2299,7 @@ class StripeTest(StripeTestCase):
|
|||
assert new_plan is not None
|
||||
|
||||
with self.assertLogs("corporate.stripe", "INFO") as m:
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
response = self.client_patch(
|
||||
"/json/billing/plan",
|
||||
{"status": CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE},
|
||||
|
@ -2488,7 +2488,7 @@ class StripeTest(StripeTestCase):
|
|||
new_plan = get_current_plan_by_realm(user.realm)
|
||||
assert new_plan is not None
|
||||
with self.assertLogs("corporate.stripe", "INFO") as m:
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
response = self.client_patch(
|
||||
"/json/billing/plan",
|
||||
{"status": CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE},
|
||||
|
@ -2602,7 +2602,7 @@ class StripeTest(StripeTestCase):
|
|||
assert new_plan is not None
|
||||
|
||||
with self.assertLogs("corporate.stripe", "INFO") as m:
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
response = self.client_patch(
|
||||
"/json/billing/plan",
|
||||
{"status": CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE},
|
||||
|
@ -2767,7 +2767,7 @@ class StripeTest(StripeTestCase):
|
|||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
||||
with self.assertLogs("corporate.stripe", "INFO") as m:
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
response = self.client_patch(
|
||||
"/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}
|
||||
)
|
||||
|
@ -2781,7 +2781,7 @@ class StripeTest(StripeTestCase):
|
|||
assert plan is not None
|
||||
self.assertEqual(plan.status, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE)
|
||||
with self.assertLogs("corporate.stripe", "INFO") as m:
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
response = self.client_patch("/json/billing/plan", {"status": CustomerPlan.ACTIVE})
|
||||
expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {stripe_customer_id}, CustomerPlan.id: {new_plan.id}, status: {CustomerPlan.ACTIVE}"
|
||||
self.assertEqual(m.output[0], expected_log)
|
||||
|
@ -2807,7 +2807,7 @@ class StripeTest(StripeTestCase):
|
|||
stripe_customer_id = Customer.objects.get(realm=user.realm).id
|
||||
new_plan = get_current_plan_by_realm(user.realm)
|
||||
assert new_plan is not None
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
self.client_patch(
|
||||
"/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}
|
||||
)
|
||||
|
@ -2848,7 +2848,7 @@ class StripeTest(StripeTestCase):
|
|||
|
||||
self.login_user(user)
|
||||
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
self.client_patch("/json/billing/plan", {"status": CustomerPlan.ENDED})
|
||||
|
||||
plan.refresh_from_db()
|
||||
|
@ -2880,7 +2880,7 @@ class StripeTest(StripeTestCase):
|
|||
|
||||
self.login_user(user)
|
||||
with self.assertLogs("corporate.stripe", "INFO") as m:
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
self.client_patch(
|
||||
"/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}
|
||||
)
|
||||
|
@ -2935,38 +2935,38 @@ class StripeTest(StripeTestCase):
|
|||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
self.upgrade(invoice=True, licenses=100)
|
||||
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
result = self.client_patch("/json/billing/plan", {"licenses": 100})
|
||||
self.assert_json_error_contains(
|
||||
result, "Your plan is already on 100 licenses in the current billing period."
|
||||
)
|
||||
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 100})
|
||||
self.assert_json_error_contains(
|
||||
result, "Your plan is already scheduled to renew with 100 licenses."
|
||||
)
|
||||
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
result = self.client_patch("/json/billing/plan", {"licenses": 50})
|
||||
self.assert_json_error_contains(
|
||||
result, "You cannot decrease the licenses in the current billing period."
|
||||
)
|
||||
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 25})
|
||||
self.assert_json_error_contains(
|
||||
result,
|
||||
"You must purchase licenses for all active users in your organization (minimum 30).",
|
||||
)
|
||||
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
result = self.client_patch("/json/billing/plan", {"licenses": 2000})
|
||||
self.assert_json_error_contains(
|
||||
result, "Invoices with more than 1000 licenses can't be processed from this page."
|
||||
)
|
||||
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
result = self.client_patch("/json/billing/plan", {"licenses": 150})
|
||||
self.assert_json_success(result)
|
||||
invoice_plans_as_needed(self.next_year)
|
||||
|
@ -3016,7 +3016,7 @@ class StripeTest(StripeTestCase):
|
|||
for key, value in line_item_params.items():
|
||||
self.assertEqual(extra_license_item.get(key), value)
|
||||
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.next_year):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.next_year):
|
||||
result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 120})
|
||||
self.assert_json_success(result)
|
||||
invoice_plans_as_needed(self.next_year + timedelta(days=365))
|
||||
|
@ -3069,7 +3069,7 @@ class StripeTest(StripeTestCase):
|
|||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
self.local_upgrade(100, False, CustomerPlan.ANNUAL, True, False)
|
||||
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
result = self.client_patch(
|
||||
"/json/billing/plan",
|
||||
{"licenses_at_next_renewal": get_latest_seat_count(user.realm) - 2},
|
||||
|
@ -3115,11 +3115,11 @@ class StripeTest(StripeTestCase):
|
|||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
||||
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
result = self.client_patch("/json/billing/plan", {"licenses": 100})
|
||||
self.assert_json_error_contains(result, "Your plan is on automatic license management.")
|
||||
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 100})
|
||||
self.assert_json_error_contains(result, "Your plan is on automatic license management.")
|
||||
|
||||
|
@ -3139,7 +3139,7 @@ class StripeTest(StripeTestCase):
|
|||
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
||||
|
||||
self.login_user(self.example_user("hamlet"))
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
response = self.client_patch("/json/billing/plan", {})
|
||||
self.assert_json_error_contains(response, "Nothing to change")
|
||||
|
||||
|
@ -3149,7 +3149,7 @@ class StripeTest(StripeTestCase):
|
|||
|
||||
self.login_user(self.example_user("hamlet"))
|
||||
with self.assertLogs("corporate.stripe", "INFO") as m:
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
result = self.client_patch(
|
||||
"/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}
|
||||
)
|
||||
|
@ -3171,7 +3171,7 @@ class StripeTest(StripeTestCase):
|
|||
|
||||
self.login_user(self.example_user("hamlet"))
|
||||
with self.assertLogs("corporate.stripe", "INFO") as m:
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
result = self.client_patch(
|
||||
"/json/billing/plan", {"status": CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE}
|
||||
)
|
||||
|
|
|
@ -4,21 +4,10 @@ from typing import Any, Dict, Optional
|
|||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from corporate.lib.stripe import (
|
||||
RealmBillingSession,
|
||||
do_change_plan_status,
|
||||
downgrade_at_the_end_of_billing_cycle,
|
||||
downgrade_now_without_creating_additional_invoices,
|
||||
get_latest_seat_count,
|
||||
update_license_ledger_for_manual_plan,
|
||||
validate_licenses,
|
||||
)
|
||||
from corporate.models import CustomerPlan, get_current_plan_by_realm, get_customer_by_realm
|
||||
from corporate.lib.stripe import RealmBillingSession, UpdatePlanRequest
|
||||
from corporate.models import CustomerPlan, get_customer_by_realm
|
||||
from zerver.decorator import require_billing_access, zulip_login_required
|
||||
from zerver.lib.exceptions import JsonableError
|
||||
from zerver.lib.request import REQ, has_request_variables
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.validator import check_int, check_int_in, check_string
|
||||
|
@ -142,96 +131,11 @@ def update_plan(
|
|||
"licenses_at_next_renewal", json_validator=check_int, default=None
|
||||
),
|
||||
) -> HttpResponse:
|
||||
plan = get_current_plan_by_realm(user.realm)
|
||||
assert plan is not None # for mypy
|
||||
|
||||
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()
|
||||
update_plan_request = UpdatePlanRequest(
|
||||
status=status,
|
||||
licenses=licenses,
|
||||
licenses_at_next_renewal=licenses_at_next_renewal,
|
||||
)
|
||||
if new_plan is not None:
|
||||
raise JsonableError(
|
||||
_("Unable to update the plan. The plan has been expired and replaced with a new plan.")
|
||||
)
|
||||
|
||||
if last_ledger_entry is None:
|
||||
raise JsonableError(_("Unable to update the plan. The plan has ended."))
|
||||
|
||||
if status is not None:
|
||||
if status == CustomerPlan.ACTIVE:
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
do_change_plan_status(plan, status)
|
||||
elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
downgrade_at_the_end_of_billing_cycle(user.realm)
|
||||
elif status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE:
|
||||
assert plan.billing_schedule == CustomerPlan.MONTHLY
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
# Customer needs to switch to an active plan first to avoid unexpected behavior.
|
||||
assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE
|
||||
assert plan.fixed_price is None
|
||||
do_change_plan_status(plan, status)
|
||||
elif status == CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE:
|
||||
assert plan.billing_schedule == CustomerPlan.ANNUAL
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
# Customer needs to switch to an active plan first to avoid unexpected behavior.
|
||||
assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE
|
||||
assert plan.fixed_price is None
|
||||
do_change_plan_status(plan, status)
|
||||
elif status == CustomerPlan.ENDED:
|
||||
assert plan.is_free_trial()
|
||||
downgrade_now_without_creating_additional_invoices(user.realm)
|
||||
billing_session = RealmBillingSession(user=user)
|
||||
billing_session.do_update_plan(update_plan_request)
|
||||
return json_success(request)
|
||||
|
||||
if licenses is not None:
|
||||
if plan.automanage_licenses:
|
||||
raise JsonableError(
|
||||
_(
|
||||
"Unable to update licenses manually. Your plan is on automatic license management."
|
||||
)
|
||||
)
|
||||
if last_ledger_entry.licenses == licenses:
|
||||
raise JsonableError(
|
||||
_(
|
||||
"Your plan is already on {licenses} licenses in the current billing period."
|
||||
).format(licenses=licenses)
|
||||
)
|
||||
if last_ledger_entry.licenses > licenses:
|
||||
raise JsonableError(
|
||||
_("You cannot decrease the licenses in the current billing period.")
|
||||
)
|
||||
validate_licenses(
|
||||
plan.charge_automatically,
|
||||
licenses,
|
||||
get_latest_seat_count(user.realm),
|
||||
plan.customer.exempt_from_license_number_check,
|
||||
)
|
||||
update_license_ledger_for_manual_plan(plan, timezone_now(), licenses=licenses)
|
||||
return json_success(request)
|
||||
|
||||
if licenses_at_next_renewal is not None:
|
||||
if plan.automanage_licenses:
|
||||
raise JsonableError(
|
||||
_(
|
||||
"Unable to update licenses manually. Your plan is on automatic license management."
|
||||
)
|
||||
)
|
||||
if last_ledger_entry.licenses_at_next_renewal == licenses_at_next_renewal:
|
||||
raise JsonableError(
|
||||
_(
|
||||
"Your plan is already scheduled to renew with {licenses_at_next_renewal} licenses."
|
||||
).format(licenses_at_next_renewal=licenses_at_next_renewal)
|
||||
)
|
||||
validate_licenses(
|
||||
plan.charge_automatically,
|
||||
licenses_at_next_renewal,
|
||||
get_latest_seat_count(user.realm),
|
||||
plan.customer.exempt_from_license_number_check,
|
||||
)
|
||||
update_license_ledger_for_manual_plan(
|
||||
plan, timezone_now(), licenses_at_next_renewal=licenses_at_next_renewal
|
||||
)
|
||||
return json_success(request)
|
||||
|
||||
raise JsonableError(_("Nothing to change."))
|
||||
|
|
|
@ -39,7 +39,7 @@ from zerver.models import (
|
|||
from zerver.tornado.django_api import send_event, send_event_on_commit
|
||||
|
||||
if settings.BILLING_ENABLED:
|
||||
from corporate.lib.stripe import downgrade_now_without_creating_additional_invoices
|
||||
from corporate.lib.stripe import RealmBillingSession
|
||||
|
||||
|
||||
def active_humans_in_realm(realm: Realm) -> QuerySet[UserProfile]:
|
||||
|
@ -309,7 +309,8 @@ def do_deactivate_realm(realm: Realm, *, acting_user: Optional[UserProfile]) ->
|
|||
realm.save(update_fields=["deactivated"])
|
||||
|
||||
if settings.BILLING_ENABLED:
|
||||
downgrade_now_without_creating_additional_invoices(realm)
|
||||
billing_session = RealmBillingSession(user=acting_user, realm=realm)
|
||||
billing_session.downgrade_now_without_creating_additional_invoices()
|
||||
|
||||
event_time = timezone_now()
|
||||
RealmAuditLog.objects.create(
|
||||
|
@ -389,7 +390,8 @@ def do_delete_all_realm_attachments(realm: Realm, *, batch_size: int = 1000) ->
|
|||
|
||||
def do_scrub_realm(realm: Realm, *, acting_user: Optional[UserProfile]) -> None:
|
||||
if settings.BILLING_ENABLED:
|
||||
downgrade_now_without_creating_additional_invoices(realm)
|
||||
billing_session = RealmBillingSession(user=acting_user, realm=realm)
|
||||
billing_session.downgrade_now_without_creating_additional_invoices()
|
||||
|
||||
users = UserProfile.objects.filter(realm=realm)
|
||||
for user in users:
|
||||
|
|
Loading…
Reference in New Issue