billing: Rename migrate_customer_to_legacy_plan.

Renames migrate_customer_to_legacy_plan to
create_complimentary_access_plan for how this
function is currently used.

Prep for adding a complimentary access plan
for Zulip Cloud.
This commit is contained in:
Lauryn Menard
2025-01-13 19:56:12 +01:00
committed by Tim Abbott
parent 757dd3e2a2
commit d3bc9bec1d
5 changed files with 65 additions and 60 deletions

View File

@@ -1438,7 +1438,7 @@ class BillingSession(ABC):
f"Cannot currently configure a courtesy plan for {self.billing_entity_display_name}." f"Cannot currently configure a courtesy plan for {self.billing_entity_display_name}."
) # nocoverage ) # nocoverage
self.migrate_customer_to_legacy_plan(plan_anchor_date, plan_end_date) self.create_complimentary_access_plan(plan_anchor_date, plan_end_date)
return f"Temporary courtesy plan for {self.billing_entity_display_name} configured to end on {end_date_string}." return f"Temporary courtesy plan for {self.billing_entity_display_name} configured to end on {end_date_string}."
def configure_fixed_price_plan(self, fixed_price: int, sent_invoice_id: str | None) -> str: def configure_fixed_price_plan(self, fixed_price: int, sent_invoice_id: str | None) -> str:
@@ -3775,23 +3775,22 @@ class BillingSession(ABC):
# needs the updated plan for a correct LicenseLedger update. # needs the updated plan for a correct LicenseLedger update.
return plan return plan
def migrate_customer_to_legacy_plan( def create_complimentary_access_plan(
self, self,
renewal_date: datetime, renewal_date: datetime,
end_date: datetime, end_date: datetime,
) -> None: ) -> None:
assert not isinstance(self, RealmBillingSession) plan_tier = CustomerPlan.TIER_SELF_HOSTED_LEGACY
if isinstance(self, RealmBillingSession): # nocoverage
# TODO implement a complimentary access plan/tier for Zulip Cloud.
return None
customer = self.update_or_create_customer() customer = self.update_or_create_customer()
# Customers on a legacy plan that is scheduled to be upgraded have 2 plans. complimentary_access_plan_anchor = renewal_date
# This plan is used to track the current status of SWITCH_PLAN_TIER_AT_PLAN_END complimentary_access_plan_params = {
# and will not charge the customer. The other plan is used to track the new plan "billing_cycle_anchor": complimentary_access_plan_anchor,
# the customer will move to the end of this plan.
legacy_plan_anchor = renewal_date
legacy_plan_params = {
"billing_cycle_anchor": legacy_plan_anchor,
"status": CustomerPlan.ACTIVE, "status": CustomerPlan.ACTIVE,
"tier": CustomerPlan.TIER_SELF_HOSTED_LEGACY, "tier": plan_tier,
# end_date and next_invoice_date should always be the same for these plans. # end_date and next_invoice_date should always be the same for these plans.
"end_date": end_date, "end_date": end_date,
"next_invoice_date": end_date, "next_invoice_date": end_date,
@@ -3802,30 +3801,32 @@ class BillingSession(ABC):
"billing_schedule": CustomerPlan.BILLING_SCHEDULE_ANNUAL, "billing_schedule": CustomerPlan.BILLING_SCHEDULE_ANNUAL,
"automanage_licenses": True, "automanage_licenses": True,
} }
legacy_plan = CustomerPlan.objects.create( complimentary_access_plan = CustomerPlan.objects.create(
customer=customer, customer=customer,
**legacy_plan_params, **complimentary_access_plan_params,
) )
try: try:
billed_licenses = self.get_billable_licenses_for_customer(customer, legacy_plan.tier) billed_licenses = self.get_billable_licenses_for_customer(
customer, complimentary_access_plan.tier
)
except MissingDataError: except MissingDataError:
billed_licenses = 0 billed_licenses = 0
# Create a ledger entry for the legacy plan for tracking purposes. # Create a ledger entry for the complimentary access plan for tracking purposes.
ledger_entry = LicenseLedger.objects.create( ledger_entry = LicenseLedger.objects.create(
plan=legacy_plan, plan=complimentary_access_plan,
is_renewal=True, is_renewal=True,
event_time=legacy_plan_anchor, event_time=complimentary_access_plan_anchor,
licenses=billed_licenses, licenses=billed_licenses,
licenses_at_next_renewal=billed_licenses, licenses_at_next_renewal=billed_licenses,
) )
legacy_plan.invoiced_through = ledger_entry complimentary_access_plan.invoiced_through = ledger_entry
legacy_plan.save(update_fields=["invoiced_through"]) complimentary_access_plan.save(update_fields=["invoiced_through"])
self.write_to_audit_log( self.write_to_audit_log(
event_type=BillingSessionEventType.CUSTOMER_PLAN_CREATED, event_type=BillingSessionEventType.CUSTOMER_PLAN_CREATED,
event_time=legacy_plan_anchor, event_time=complimentary_access_plan_anchor,
extra_data=legacy_plan_params, extra_data=complimentary_access_plan_params,
) )
self.do_change_plan_type(tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY, is_sponsored=False) self.do_change_plan_type(tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY, is_sponsored=False)

View File

@@ -729,7 +729,7 @@ class RemoteBillingAuthenticationTest(RemoteRealmBillingTestCase):
) )
@responses.activate @responses.activate
def test_transfer_legacy_plan_scheduled_for_upgrade_from_server_to_realm( def test_transfer_complimentary_access_plan_scheduled_for_upgrade_from_server_to_realm(
self, self,
) -> None: ) -> None:
self.login("desdemona") self.login("desdemona")
@@ -741,9 +741,9 @@ class RemoteBillingAuthenticationTest(RemoteRealmBillingTestCase):
start_date = timezone_now() start_date = timezone_now()
end_date = add_months(timezone_now(), 10) end_date = add_months(timezone_now(), 10)
# Migrate server to legacy to plan. # Migrate server to complimentary access plan.
server_billing_session = RemoteServerBillingSession(self.server) server_billing_session = RemoteServerBillingSession(self.server)
server_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) server_billing_session.create_complimentary_access_plan(start_date, end_date)
server_customer = server_billing_session.get_customer() server_customer = server_billing_session.get_customer()
assert server_customer is not None assert server_customer is not None
@@ -1151,11 +1151,11 @@ class RemoteBillingAuthenticationTest(RemoteRealmBillingTestCase):
# Server still has no plan. # Server still has no plan.
self.assertIsNone(get_current_plan_by_customer(server_customer)) self.assertIsNone(get_current_plan_by_customer(server_customer))
# CASE: Server has legacy plan but all realms are deactivated. # CASE: Server has complimentary access plan but all realms are deactivated.
start_date = timezone_now() start_date = timezone_now()
end_date = add_months(timezone_now(), 10) end_date = add_months(timezone_now(), 10)
server_billing_session = RemoteServerBillingSession(self.server) server_billing_session = RemoteServerBillingSession(self.server)
server_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) server_billing_session.create_complimentary_access_plan(start_date, end_date)
# All realms are deactivated. # All realms are deactivated.
Realm.objects.all().update(deactivated=True) Realm.objects.all().update(deactivated=True)

View File

@@ -8048,11 +8048,11 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
remote_realm = RemoteRealm.objects.get(uuid=hamlet.realm.uuid) remote_realm = RemoteRealm.objects.get(uuid=hamlet.realm.uuid)
remote_realm_billing_session = RemoteRealmBillingSession(remote_realm=remote_realm) remote_realm_billing_session = RemoteRealmBillingSession(remote_realm=remote_realm)
# Migrate realm to legacy plan. # Create complimentary access plan for realm.
with time_machine.travel(self.now, tick=False): with time_machine.travel(self.now, tick=False):
start_date = timezone_now() start_date = timezone_now()
end_date = add_months(start_date, months=3) end_date = add_months(start_date, months=3)
remote_realm_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) remote_realm_billing_session.create_complimentary_access_plan(start_date, end_date)
self.add_mock_response() self.add_mock_response()
with time_machine.travel(self.now, tick=False): with time_machine.travel(self.now, tick=False):
@@ -8382,11 +8382,11 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
remote_server = RemoteZulipServer.objects.get(hostname="demo.example.com") remote_server = RemoteZulipServer.objects.get(hostname="demo.example.com")
server_billing_session = RemoteServerBillingSession(remote_server=remote_server) server_billing_session = RemoteServerBillingSession(remote_server=remote_server)
# Migrate server to legacy plan. # Create complimentary access plan for server.
with time_machine.travel(self.now, tick=False): with time_machine.travel(self.now, tick=False):
start_date = timezone_now() start_date = timezone_now()
end_date = add_months(start_date, months=3) end_date = add_months(start_date, months=3)
server_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) server_billing_session.create_complimentary_access_plan(start_date, end_date)
server_customer = server_billing_session.get_customer() server_customer = server_billing_session.get_customer()
assert server_customer is not None assert server_customer is not None
@@ -8659,11 +8659,11 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
remote_realm = RemoteRealm.objects.get(uuid=hamlet.realm.uuid) remote_realm = RemoteRealm.objects.get(uuid=hamlet.realm.uuid)
remote_realm_billing_session = RemoteRealmBillingSession(remote_realm=remote_realm) remote_realm_billing_session = RemoteRealmBillingSession(remote_realm=remote_realm)
# Migrate realm to legacy plan. # Create complimentary access plan for realm.
with time_machine.travel(self.now, tick=False): with time_machine.travel(self.now, tick=False):
start_date = timezone_now() start_date = timezone_now()
end_date = add_months(start_date, months=3) end_date = add_months(start_date, months=3)
remote_realm_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) remote_realm_billing_session.create_complimentary_access_plan(start_date, end_date)
# Upload data. # Upload data.
self.add_mock_response() self.add_mock_response()
@@ -9011,11 +9011,11 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
self.add_mock_response() self.add_mock_response()
send_server_data_to_push_bouncer(consider_usage_statistics=False) send_server_data_to_push_bouncer(consider_usage_statistics=False)
# Migrate server to legacy plan. # Create complimentary access plan for server.
with time_machine.travel(self.now, tick=False): with time_machine.travel(self.now, tick=False):
start_date = timezone_now() start_date = timezone_now()
end_date = add_months(start_date, months=3) end_date = add_months(start_date, months=3)
self.billing_session.migrate_customer_to_legacy_plan(start_date, end_date) self.billing_session.create_complimentary_access_plan(start_date, end_date)
customer = self.billing_session.get_customer() customer = self.billing_session.get_customer()
assert customer is not None assert customer is not None
@@ -9107,9 +9107,9 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
with time_machine.travel(self.now, tick=False): with time_machine.travel(self.now, tick=False):
send_server_data_to_push_bouncer(consider_usage_statistics=False) send_server_data_to_push_bouncer(consider_usage_statistics=False)
# Test that free trial is not available for customers with active legacy plan. # Free trial is not available for customers with active complimentary access plan.
end_date = add_months(self.now, months=3) end_date = add_months(self.now, months=3)
self.billing_session.migrate_customer_to_legacy_plan(self.now, end_date) self.billing_session.create_complimentary_access_plan(self.now, end_date)
result = self.execute_remote_billing_authentication_flow( result = self.execute_remote_billing_authentication_flow(
hamlet.delivery_email, hamlet.full_name hamlet.delivery_email, hamlet.full_name
@@ -9151,9 +9151,9 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
with time_machine.travel(self.now, tick=False): with time_machine.travel(self.now, tick=False):
send_server_data_to_push_bouncer(consider_usage_statistics=False) send_server_data_to_push_bouncer(consider_usage_statistics=False)
# Test that free trial is not available for customers with active legacy plan. # Free trial is not available for customers with active complimentary access plan.
end_date = add_months(self.now, months=3) end_date = add_months(self.now, months=3)
self.billing_session.migrate_customer_to_legacy_plan(self.now, end_date) self.billing_session.create_complimentary_access_plan(self.now, end_date)
CustomerPlan.objects.filter(customer__remote_server=self.remote_server).update( CustomerPlan.objects.filter(customer__remote_server=self.remote_server).update(
status=CustomerPlan.ENDED status=CustomerPlan.ENDED
) )
@@ -9904,9 +9904,9 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
self.login("hamlet") self.login("hamlet")
hamlet = self.example_user("hamlet") hamlet = self.example_user("hamlet")
# Migrate server to legacy plan. # Create complimentary access plan for server.
end_date = add_months(self.now, months=3) end_date = add_months(self.now, months=3)
self.billing_session.migrate_customer_to_legacy_plan(self.now, end_date) self.billing_session.create_complimentary_access_plan(self.now, end_date)
self.add_mock_response() self.add_mock_response()
with time_machine.travel(self.now, tick=False): with time_machine.travel(self.now, tick=False):
@@ -10190,9 +10190,9 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
send_server_data_to_push_bouncer(consider_usage_statistics=False) send_server_data_to_push_bouncer(consider_usage_statistics=False)
plan_end_date = add_months(self.now, 3) plan_end_date = add_months(self.now, 3)
self.billing_session.migrate_customer_to_legacy_plan(self.now, plan_end_date) self.billing_session.create_complimentary_access_plan(self.now, plan_end_date)
# Legacy plan ends on plan end date. # Complimentary access plan ends on plan end date.
customer = self.billing_session.get_customer() customer = self.billing_session.get_customer()
assert customer is not None assert customer is not None
plan = get_current_plan_by_customer(customer) plan = get_current_plan_by_customer(customer)
@@ -10231,11 +10231,11 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
with time_machine.travel(self.now, tick=False): with time_machine.travel(self.now, tick=False):
send_server_data_to_push_bouncer(consider_usage_statistics=False) send_server_data_to_push_bouncer(consider_usage_statistics=False)
# Migrate server to legacy plan. # Create complimentary access plan for server.
with time_machine.travel(self.now, tick=False): with time_machine.travel(self.now, tick=False):
start_date = timezone_now() start_date = timezone_now()
end_date = add_months(start_date, months=3) end_date = add_months(start_date, months=3)
self.billing_session.migrate_customer_to_legacy_plan(start_date, end_date) self.billing_session.create_complimentary_access_plan(start_date, end_date)
customer = self.billing_session.get_customer() customer = self.billing_session.get_customer()
assert customer is not None assert customer is not None

View File

@@ -70,44 +70,48 @@ class TestRemoteServerSupportEndpoint(ZulipTestCase):
requested_plan=plan, requested_plan=plan,
) )
def upgrade_legacy_plan(legacy_plan: CustomerPlan) -> None: def upgrade_complimentary_access_plan(complimentary_access_plan: CustomerPlan) -> None:
billed_licenses = 10 billed_licenses = 10
assert legacy_plan.end_date is not None assert complimentary_access_plan.end_date is not None
last_ledger_entry = ( last_ledger_entry = (
LicenseLedger.objects.filter(plan=legacy_plan).order_by("-id").first() LicenseLedger.objects.filter(plan=complimentary_access_plan).order_by("-id").first()
) )
assert last_ledger_entry is not None assert last_ledger_entry is not None
last_ledger_entry.licenses_at_next_renewal = billed_licenses last_ledger_entry.licenses_at_next_renewal = billed_licenses
last_ledger_entry.save(update_fields=["licenses_at_next_renewal"]) last_ledger_entry.save(update_fields=["licenses_at_next_renewal"])
legacy_plan.status = CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END complimentary_access_plan.status = CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END
legacy_plan.save(update_fields=["status"]) complimentary_access_plan.save(update_fields=["status"])
plan_params = { plan_params = {
"automanage_licenses": True, "automanage_licenses": True,
"charge_automatically": False, "charge_automatically": False,
"price_per_license": 100, "price_per_license": 100,
"billing_cycle_anchor": legacy_plan.end_date, "billing_cycle_anchor": complimentary_access_plan.end_date,
"billing_schedule": CustomerPlan.BILLING_SCHEDULE_MONTHLY, "billing_schedule": CustomerPlan.BILLING_SCHEDULE_MONTHLY,
"tier": CustomerPlan.TIER_SELF_HOSTED_BASIC, "tier": CustomerPlan.TIER_SELF_HOSTED_BASIC,
"status": CustomerPlan.NEVER_STARTED, "status": CustomerPlan.NEVER_STARTED,
} }
CustomerPlan.objects.create( CustomerPlan.objects.create(
customer=legacy_plan.customer, next_invoice_date=legacy_plan.end_date, **plan_params customer=complimentary_access_plan.customer,
next_invoice_date=complimentary_access_plan.end_date,
**plan_params,
) )
def add_legacy_plan(name: str, upgrade: bool) -> None: def add_complimentary_access_plan(name: str, upgrade: bool) -> None:
legacy_anchor = datetime(2050, 1, 1, tzinfo=timezone.utc) complimentary_access_plan_anchor = datetime(2050, 1, 1, tzinfo=timezone.utc)
next_plan_anchor = datetime(2050, 2, 1, tzinfo=timezone.utc) next_plan_anchor = datetime(2050, 2, 1, tzinfo=timezone.utc)
remote_realm = RemoteRealm.objects.get(name=name) remote_realm = RemoteRealm.objects.get(name=name)
billing_session = RemoteRealmBillingSession(remote_realm) billing_session = RemoteRealmBillingSession(remote_realm)
billing_session.migrate_customer_to_legacy_plan(legacy_anchor, next_plan_anchor) billing_session.create_complimentary_access_plan(
complimentary_access_plan_anchor, next_plan_anchor
)
customer = billing_session.get_customer() customer = billing_session.get_customer()
assert customer is not None assert customer is not None
complimentary_access_plan = billing_session.get_complimentary_access_plan(customer) complimentary_access_plan = billing_session.get_complimentary_access_plan(customer)
assert complimentary_access_plan is not None assert complimentary_access_plan is not None
assert complimentary_access_plan.end_date is not None assert complimentary_access_plan.end_date is not None
if upgrade: if upgrade:
upgrade_legacy_plan(complimentary_access_plan) upgrade_complimentary_access_plan(complimentary_access_plan)
super().setUp() super().setUp()
@@ -158,11 +162,11 @@ class TestRemoteServerSupportEndpoint(ZulipTestCase):
plan=SponsoredPlanTypes.COMMUNITY.value, plan=SponsoredPlanTypes.COMMUNITY.value,
) )
# Add expected legacy customer and plan data: # Add expected customer and plan data:
# with upgrade scheduled # with upgrade scheduled
add_legacy_plan(name="realm-name-4", upgrade=True) add_complimentary_access_plan(name="realm-name-4", upgrade=True)
# without upgrade scheduled # without upgrade scheduled
add_legacy_plan(name="realm-name-5", upgrade=False) add_complimentary_access_plan(name="realm-name-5", upgrade=False)
# Add billing users # Add billing users
remote_realm = RemoteRealm.objects.get(name="realm-name-3") remote_realm = RemoteRealm.objects.get(name="realm-name-3")

View File

@@ -469,7 +469,7 @@ def populate_remote_server(customer_profile: CustomerProfile) -> dict[str, str]:
end_date = datetime.strptime(customer_profile.end_date, TIMESTAMP_FORMAT).replace( end_date = datetime.strptime(customer_profile.end_date, TIMESTAMP_FORMAT).replace(
tzinfo=timezone.utc tzinfo=timezone.utc
) )
billing_session.migrate_customer_to_legacy_plan(renewal_date, end_date) billing_session.create_complimentary_access_plan(renewal_date, end_date)
if not communicate_with_stripe: if not communicate_with_stripe:
# We need to communicate with stripe to upgrade here. # We need to communicate with stripe to upgrade here.
@@ -567,7 +567,7 @@ def populate_remote_realms(customer_profile: CustomerProfile) -> dict[str, str]:
end_date = datetime.strptime(customer_profile.end_date, TIMESTAMP_FORMAT).replace( end_date = datetime.strptime(customer_profile.end_date, TIMESTAMP_FORMAT).replace(
tzinfo=timezone.utc tzinfo=timezone.utc
) )
billing_session.migrate_customer_to_legacy_plan(renewal_date, end_date) billing_session.create_complimentary_access_plan(renewal_date, end_date)
elif customer_profile.tier is not None: elif customer_profile.tier is not None:
billing_session.do_change_plan_type( billing_session.do_change_plan_type(
tier=customer_profile.tier, is_sponsored=customer_profile.is_sponsored tier=customer_profile.tier, is_sponsored=customer_profile.is_sponsored