diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py
index 65c17eed6b..4cbb191e3d 100644
--- a/corporate/lib/stripe.py
+++ b/corporate/lib/stripe.py
@@ -588,6 +588,8 @@ class UpgradePageParams(TypedDict):
seat_count: int
billing_base_url: str
tier: int
+ flat_discount: int
+ flat_discounted_months: int
class UpgradePageSessionTypeSpecificContext(TypedDict):
@@ -706,9 +708,11 @@ class BillingSession(ABC):
def get_data_for_stripe_payment_intent(
self,
+ customer: Customer,
price_per_license: int,
licenses: int,
plan_tier: int,
+ billing_schedule: int,
email: str,
) -> StripePaymentIntentData:
if hasattr(self, "support_session") and self.support_session: # nocoverage
@@ -721,6 +725,12 @@ class BillingSession(ABC):
plan_name = CustomerPlan.name_from_tier(plan_tier)
description = f"Upgrade to {plan_name}, ${price_per_license/100} x {licenses}"
+ if customer.flat_discounted_months > 0:
+ num_months = 12 if billing_schedule == CustomerPlan.BILLING_SCHEDULE_ANNUAL else 1
+ flat_discounted_months = min(customer.flat_discounted_months, num_months)
+ amount -= customer.flat_discount * flat_discounted_months
+ description += f" - ${customer.flat_discount/100} x {flat_discounted_months}"
+
return StripePaymentIntentData(
amount=amount,
description=description,
@@ -917,7 +927,12 @@ class BillingSession(ABC):
customer = self.get_customer()
assert customer is not None and customer.stripe_customer_id is not None
payment_intent_data = self.get_data_for_stripe_payment_intent(
- price_per_license, licenses, metadata["plan_tier"], self.get_email()
+ customer,
+ price_per_license,
+ licenses,
+ metadata["plan_tier"],
+ metadata["billing_schedule"],
+ self.get_email(),
)
# Ensure customers have a default payment method set.
stripe_customer = stripe_get_customer(customer.stripe_customer_id)
@@ -1021,14 +1036,18 @@ class BillingSession(ABC):
plan.save(update_fields=["discount", "price_per_license"])
def attach_discount_to_customer(self, new_discount: Decimal) -> str:
+ # Remove flat discount if giving customer a percentage discount.
customer = self.get_customer()
old_discount = None
if customer is not None:
old_discount = customer.default_discount
customer.default_discount = new_discount
- customer.save(update_fields=["default_discount"])
+ customer.flat_discounted_months = 0
+ customer.save(update_fields=["default_discount", "flat_discounted_months"])
else:
- customer = self.update_or_create_customer(defaults={"default_discount": new_discount})
+ customer = self.update_or_create_customer(
+ defaults={"default_discount": new_discount, "flat_discounted_months": 0}
+ )
plan = get_current_plan_by_customer(customer)
if plan is not None:
self.apply_discount_to_plan(plan, new_discount)
@@ -1305,6 +1324,21 @@ class BillingSession(ABC):
unit_amount=price_per_license,
)
+ if customer.flat_discounted_months > 0:
+ num_months = 12 if billing_schedule == CustomerPlan.BILLING_SCHEDULE_ANNUAL else 1
+ flat_discounted_months = min(customer.flat_discounted_months, num_months)
+ discount = customer.flat_discount * flat_discounted_months
+ customer.flat_discounted_months -= flat_discounted_months
+ customer.save(update_fields=["flat_discounted_months"])
+
+ stripe.InvoiceItem.create(
+ currency="usd",
+ customer=customer.stripe_customer_id,
+ description=f"${customer.flat_discount}/month new customer discount",
+ # Negative value to apply discount.
+ amount=(-1 * discount),
+ )
+
if charge_automatically:
collection_method = "charge_automatically"
days_until_due = None
@@ -1717,18 +1751,23 @@ class BillingSession(ABC):
billing_frequency = CustomerPlan.BILLING_SCHEDULES[plan.billing_schedule]
if switch_to_annual_at_end_of_cycle:
+ num_months_next_cycle = 12
annual_price_per_license = get_price_per_license(
plan.tier, CustomerPlan.BILLING_SCHEDULE_ANNUAL, customer.default_discount
)
renewal_cents = annual_price_per_license * licenses_at_next_renewal
price_per_license = format_money(annual_price_per_license / 12)
elif switch_to_monthly_at_end_of_cycle:
+ num_months_next_cycle = 1
monthly_price_per_license = get_price_per_license(
plan.tier, CustomerPlan.BILLING_SCHEDULE_MONTHLY, customer.default_discount
)
renewal_cents = monthly_price_per_license * licenses_at_next_renewal
price_per_license = format_money(monthly_price_per_license)
else:
+ num_months_next_cycle = (
+ 12 if plan.billing_schedule == CustomerPlan.BILLING_SCHEDULE_ANNUAL else 1
+ )
renewal_cents = self.get_customer_plan_renewal_amount(plan, now, last_ledger_entry)
if plan.price_per_license is None:
@@ -1738,6 +1777,14 @@ class BillingSession(ABC):
else:
price_per_license = format_money(plan.price_per_license)
+ # TODO: Do this calculation in `invoice_plan` too.
+ pre_discount_renewal_cents = renewal_cents
+ flat_discount, flat_discounted_months = self.get_flat_discount_info(plan.customer)
+ if flat_discounted_months > 0:
+ flat_discounted_months = min(flat_discounted_months, num_months_next_cycle)
+ discount = flat_discount * flat_discounted_months
+ renewal_cents = renewal_cents - discount
+
charge_automatically = plan.charge_automatically
assert customer.stripe_customer_id is not None # for mypy
stripe_customer = stripe_get_customer(customer.stripe_customer_id)
@@ -1786,6 +1833,9 @@ class BillingSession(ABC):
"legacy_remote_server_next_plan_name": legacy_remote_server_next_plan_name,
"using_min_licenses_for_plan": using_min_licenses_for_plan,
"min_licenses_for_plan": min_licenses_for_plan,
+ "pre_discount_renewal_cents": cents_to_dollar_string(pre_discount_renewal_cents),
+ "flat_discount": format_money(customer.flat_discount),
+ "discounted_months_left": customer.flat_discounted_months,
}
return context
@@ -1821,12 +1871,30 @@ class BillingSession(ABC):
"price_per_license",
"discount_percent",
"using_min_licenses_for_plan",
+ "min_licenses_for_plan",
+ "pre_discount_renewal_cents",
]
for key in keys:
context[key] = next_plan_context[key]
return context
+ def get_flat_discount_info(self, customer: Optional[Customer] = None) -> Tuple[int, int]:
+ is_self_hosted_billing = not isinstance(self, RealmBillingSession)
+ flat_discount = 0
+ flat_discounted_months = 0
+ if is_self_hosted_billing and (customer is None or customer.flat_discounted_months > 0):
+ if customer is None:
+ temp_customer = Customer()
+ flat_discount = temp_customer.flat_discount
+ flat_discounted_months = 12
+ else:
+ flat_discount = customer.flat_discount
+ flat_discounted_months = customer.flat_discounted_months
+ assert isinstance(flat_discount, int)
+ assert isinstance(flat_discounted_months, int)
+ return flat_discount, flat_discounted_months
+
def get_initial_upgrade_context(
self, initial_upgrade_request: InitialUpgradeRequest
) -> Tuple[Optional[str], Optional[UpgradePageContext]]:
@@ -1893,6 +1961,7 @@ class BillingSession(ABC):
f"{free_trial_end:%B} {free_trial_end.day}, {free_trial_end.year}"
)
+ flat_discount, flat_discounted_months = self.get_flat_discount_info(customer)
context: UpgradePageContext = {
"customer_name": customer_specific_context["customer_name"],
"default_invoice_days_until_due": DEFAULT_INVOICE_DAYS_UNTIL_DUE,
@@ -1917,6 +1986,8 @@ class BillingSession(ABC):
"seat_count": seat_count,
"billing_base_url": self.billing_base_url,
"tier": tier,
+ "flat_discount": flat_discount,
+ "flat_discounted_months": flat_discounted_months,
},
"using_min_licenses_for_plan": using_min_licenses_for_plan,
"min_licenses_for_plan": min_licenses_for_plan,
@@ -3245,12 +3316,16 @@ class RemoteRealmBillingSession(BillingSession):
remote_realm=self.remote_realm,
defaults={"stripe_customer_id": stripe_customer_id},
)
- return customer
else:
customer, created = Customer.objects.update_or_create(
remote_realm=self.remote_realm, defaults=defaults
)
- return customer
+
+ if created and not customer.default_discount:
+ customer.flat_discounted_months = 12
+ customer.save(update_fields=["flat_discounted_months"])
+
+ return customer
@override
@transaction.atomic
@@ -3634,12 +3709,16 @@ class RemoteServerBillingSession(BillingSession):
remote_server=self.remote_server,
defaults={"stripe_customer_id": stripe_customer_id},
)
- return customer
else:
customer, created = Customer.objects.update_or_create(
remote_server=self.remote_server, defaults=defaults
)
- return customer
+
+ if created and not customer.default_discount:
+ customer.flat_discounted_months = 12
+ customer.save(update_fields=["flat_discounted_months"])
+
+ return customer
@override
@transaction.atomic
diff --git a/corporate/migrations/0031_customer_flat_discount_and_more.py b/corporate/migrations/0031_customer_flat_discount_and_more.py
new file mode 100644
index 0000000000..719615a622
--- /dev/null
+++ b/corporate/migrations/0031_customer_flat_discount_and_more.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.8 on 2023-12-19 12:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("corporate", "0030_alter_zulipsponsorshiprequest_requested_plan"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="customer",
+ name="flat_discount",
+ field=models.IntegerField(default=2000),
+ ),
+ migrations.AddField(
+ model_name="customer",
+ name="flat_discounted_months",
+ field=models.IntegerField(default=0),
+ ),
+ ]
diff --git a/corporate/models.py b/corporate/models.py
index 0d9a57c0dc..b0c1381190 100644
--- a/corporate/models.py
+++ b/corporate/models.py
@@ -34,6 +34,11 @@ class Customer(models.Model):
# they purchased.
exempt_from_license_number_check = models.BooleanField(default=False)
+ # In cents.
+ flat_discount = models.IntegerField(default=2000)
+ # Number of months left in the flat discount period.
+ flat_discounted_months = models.IntegerField(default=0)
+
class Meta:
# Enforce that at least one of these is set.
constraints = [
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.create.1.json
index 856442bd13..3d6b9bc66c 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.create.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.modify.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.modify.1.json
index 6c64e0b859..828b03f680 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.modify.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.modify.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.1.json
index 50bf46243d..33f1a5cc6c 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.2.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.2.json
index 50bf46243d..33f1a5cc6c 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.2.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.2.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.3.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.3.json
index 50bf46243d..33f1a5cc6c 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.3.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.3.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.4.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.4.json
index 50bf46243d..33f1a5cc6c 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.4.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.4.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.5.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.5.json
index 81c544699c..d2e9309f6a 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.5.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Customer.retrieve.5.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.1.json
index 2496249011..a78f73dbd7 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.2.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.2.json
index 0358ea62c2..97a939c624 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.2.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.2.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.3.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.3.json
index cfe538d1fa..22039a294a 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.3.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.3.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.4.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.4.json
index cc6a646b8f..b5c6ad8c81 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.4.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.4.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.5.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.5.json
index 8c9bb73ac7..3696578a0e 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.5.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Event.list.5.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.create.1.json
index dee6dd2852..5d072c4ee5 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.create.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.finalize_invoice.1.json
index 2c146c2ff2..6774868ae2 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.finalize_invoice.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--Invoice.finalize_invoice.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.1.json
index 76bc062d9c..c351f1fdd5 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.2.json
index ac37773215..f6b553cfa6 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.2.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.2.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.3.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.3.json
new file mode 100644
index 0000000000..e23b26f397
Binary files /dev/null and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--InvoiceItem.create.3.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--PaymentIntent.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--PaymentIntent.create.1.json
index 1b346aa07b..797a278c88 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--PaymentIntent.create.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--PaymentIntent.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.create.1.json
index 07472c7cde..cac481c860 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.create.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.list.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.list.1.json
index 85762d7b11..86512779df 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.list.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.retrieve.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.retrieve.1.json
index 07472c7cde..cac481c860 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.retrieve.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--SetupIntent.retrieve.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.create.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.create.1.json
index 952a0d9ad2..a2510cf57e 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.create.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.list.1.json b/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.list.1.json
index c27d5b40f9..0fbad52a65 100644
Binary files a/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.list.1.json and b/corporate/tests/stripe_fixtures/non_sponsorship_billing--checkout.Session.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.modify.1.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.modify.1.json
index 8eb8ed7521..937476bea2 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.modify.1.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.modify.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.1.json
index 627cebb879..1b2fa2b4e5 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.1.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.2.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.2.json
index 627cebb879..1b2fa2b4e5 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.2.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.2.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.3.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.3.json
index 627cebb879..1b2fa2b4e5 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.3.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.3.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.4.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.4.json
index 627cebb879..1b2fa2b4e5 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.4.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.4.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.5.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.5.json
index 627cebb879..1b2fa2b4e5 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.5.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.5.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.6.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.6.json
index 627cebb879..1b2fa2b4e5 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.6.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--Customer.retrieve.6.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.create.1.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.create.1.json
index c00c853dd8..ee193c8508 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.create.1.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.list.1.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.list.1.json
index 07261c6f3a..0e8cdc74dc 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.list.1.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.retrieve.1.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.retrieve.1.json
index c00c853dd8..ee193c8508 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.retrieve.1.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--SetupIntent.retrieve.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--checkout.Session.create.1.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--checkout.Session.create.1.json
index f151a7d554..3ade166d1a 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--checkout.Session.create.1.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--checkout.Session.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--checkout.Session.list.1.json b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--checkout.Session.list.1.json
index 8bc8cb038e..b9a56f5470 100644
Binary files a/corporate/tests/stripe_fixtures/upgrade_legacy_plan--checkout.Session.list.1.json and b/corporate/tests/stripe_fixtures/upgrade_legacy_plan--checkout.Session.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.create.1.json
new file mode 100644
index 0000000000..a5a64c5447
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.modify.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.modify.1.json
new file mode 100644
index 0000000000..768598b513
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.modify.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.1.json
new file mode 100644
index 0000000000..2deed286cd
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.2.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.2.json
new file mode 100644
index 0000000000..2deed286cd
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.2.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.3.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.3.json
new file mode 100644
index 0000000000..2deed286cd
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.3.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.4.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.4.json
new file mode 100644
index 0000000000..2deed286cd
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.4.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.5.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.5.json
new file mode 100644
index 0000000000..daa0843dc2
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.5.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.6.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.6.json
new file mode 100644
index 0000000000..daa0843dc2
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Customer.retrieve.6.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.1.json
new file mode 100644
index 0000000000..50f0bb123a
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.2.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.2.json
new file mode 100644
index 0000000000..7610e06a81
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.2.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.3.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.3.json
new file mode 100644
index 0000000000..512968a3df
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.3.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.4.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.4.json
new file mode 100644
index 0000000000..a509e3f719
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.4.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.5.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.5.json
new file mode 100644
index 0000000000..abd0df7af8
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.5.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.6.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.6.json
new file mode 100644
index 0000000000..6d922067af
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Event.list.6.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Invoice.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Invoice.create.1.json
new file mode 100644
index 0000000000..5d072c4ee5
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Invoice.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Invoice.finalize_invoice.1.json
new file mode 100644
index 0000000000..84b5302fda
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Invoice.finalize_invoice.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Invoice.list.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Invoice.list.1.json
new file mode 100644
index 0000000000..e39960ab72
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--Invoice.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--InvoiceItem.create.1.json
new file mode 100644
index 0000000000..c351f1fdd5
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--InvoiceItem.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--InvoiceItem.create.2.json
new file mode 100644
index 0000000000..f6b553cfa6
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--InvoiceItem.create.2.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--InvoiceItem.create.3.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--InvoiceItem.create.3.json
new file mode 100644
index 0000000000..e23b26f397
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--InvoiceItem.create.3.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--PaymentIntent.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--PaymentIntent.create.1.json
new file mode 100644
index 0000000000..5b6cc46085
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--PaymentIntent.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--SetupIntent.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--SetupIntent.create.1.json
new file mode 100644
index 0000000000..a67bd95a00
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--SetupIntent.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--SetupIntent.list.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--SetupIntent.list.1.json
new file mode 100644
index 0000000000..5933eba6ed
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--SetupIntent.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--SetupIntent.retrieve.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--SetupIntent.retrieve.1.json
new file mode 100644
index 0000000000..a67bd95a00
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--SetupIntent.retrieve.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--checkout.Session.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--checkout.Session.create.1.json
new file mode 100644
index 0000000000..22f3d590e9
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--checkout.Session.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--checkout.Session.list.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--checkout.Session.list.1.json
new file mode 100644
index 0000000000..3f8069fcfb
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_business_plan--checkout.Session.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.create.1.json
new file mode 100644
index 0000000000..a5a64c5447
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.modify.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.modify.1.json
new file mode 100644
index 0000000000..b2ee3ebb2a
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.modify.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.1.json
new file mode 100644
index 0000000000..4396554c44
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.2.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.2.json
new file mode 100644
index 0000000000..4396554c44
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.2.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.3.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.3.json
new file mode 100644
index 0000000000..4396554c44
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.3.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.4.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.4.json
new file mode 100644
index 0000000000..4396554c44
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.4.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.5.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.5.json
new file mode 100644
index 0000000000..b4d2618bef
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.5.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.6.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.6.json
new file mode 100644
index 0000000000..b4d2618bef
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Customer.retrieve.6.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.1.json
new file mode 100644
index 0000000000..bc0bb766df
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.2.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.2.json
new file mode 100644
index 0000000000..7e5d292700
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.2.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.3.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.3.json
new file mode 100644
index 0000000000..b83e5557ec
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.3.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.4.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.4.json
new file mode 100644
index 0000000000..2818c9da6e
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.4.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.5.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.5.json
new file mode 100644
index 0000000000..0e2d5d68ad
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.5.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.6.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.6.json
new file mode 100644
index 0000000000..6d922067af
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Event.list.6.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Invoice.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Invoice.create.1.json
new file mode 100644
index 0000000000..2b0ad2bb0e
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Invoice.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Invoice.finalize_invoice.1.json
new file mode 100644
index 0000000000..288a25a124
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Invoice.finalize_invoice.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Invoice.list.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Invoice.list.1.json
new file mode 100644
index 0000000000..e39960ab72
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--Invoice.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--InvoiceItem.create.1.json
new file mode 100644
index 0000000000..041633b56c
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--InvoiceItem.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--InvoiceItem.create.2.json
new file mode 100644
index 0000000000..82e3bb8ef9
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--InvoiceItem.create.2.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--InvoiceItem.create.3.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--InvoiceItem.create.3.json
new file mode 100644
index 0000000000..a29feb75ef
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--InvoiceItem.create.3.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--PaymentIntent.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--PaymentIntent.create.1.json
new file mode 100644
index 0000000000..eb09be48ad
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--PaymentIntent.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--SetupIntent.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--SetupIntent.create.1.json
new file mode 100644
index 0000000000..7854fd6e2d
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--SetupIntent.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--SetupIntent.list.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--SetupIntent.list.1.json
new file mode 100644
index 0000000000..42b1947922
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--SetupIntent.list.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--SetupIntent.retrieve.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--SetupIntent.retrieve.1.json
new file mode 100644
index 0000000000..7854fd6e2d
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--SetupIntent.retrieve.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--checkout.Session.create.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--checkout.Session.create.1.json
new file mode 100644
index 0000000000..8960c5ec4b
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--checkout.Session.create.1.json differ
diff --git a/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--checkout.Session.list.1.json b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--checkout.Session.list.1.json
new file mode 100644
index 0000000000..2e6736d9ce
Binary files /dev/null and b/corporate/tests/stripe_fixtures/upgrade_user_to_monthly_basic_plan--checkout.Session.list.1.json differ
diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py
index bc0b0eaf44..310ec9eac4 100644
--- a/corporate/tests/test_stripe.py
+++ b/corporate/tests/test_stripe.py
@@ -579,14 +579,14 @@ class StripeTestCase(ZulipTestCase):
**kwargs: Any,
) -> "TestHttpResponse":
if upgrade_page_response is None:
+ tier = kwargs.get("tier")
+ upgrade_url = f"{self.billing_session.billing_base_url}/upgrade/"
+ if tier:
+ upgrade_url += f"?tier={tier}"
if self.billing_session.billing_base_url:
- upgrade_page_response = self.client_get(
- f"{self.billing_session.billing_base_url}/upgrade/", {}, subdomain="selfhosting"
- )
+ upgrade_page_response = self.client_get(upgrade_url, {}, subdomain="selfhosting")
else:
- upgrade_page_response = self.client_get(
- f"{self.billing_session.billing_base_url}/upgrade/", {}
- )
+ upgrade_page_response = self.client_get(upgrade_url, {})
params: Dict[str, Any] = {
"schedule": "annual",
"signed_seat_count": self.get_signed_seat_count_from_response(upgrade_page_response),
@@ -5651,7 +5651,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
@responses.activate
@mock_stripe()
- def test_non_sponsorship_billing(self, *mocks: Mock) -> None:
+ def test_upgrade_user_to_business_plan(self, *mocks: Mock) -> None:
self.login("hamlet")
hamlet = self.example_user("hamlet")
@@ -5659,6 +5659,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
realm_user_count = UserProfile.objects.filter(
realm=hamlet.realm, is_bot=False, is_active=True
).count()
+ self.assertEqual(realm_user_count, 11)
with time_machine.travel(self.now, tick=False):
send_server_data_to_push_bouncer(consider_usage_statistics=False)
@@ -5673,7 +5674,24 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
f"{self.billing_session.billing_base_url}/upgrade/", subdomain="selfhosting"
)
self.assertEqual(result.status_code, 200)
- self.assert_in_success_response(["Add card", "Purchase Zulip Business"], result)
+
+ # Min licenses used since org has less users.
+ min_licenses = self.billing_session.min_licenses_for_plan(
+ CustomerPlan.TIER_SELF_HOSTED_BUSINESS
+ )
+ self.assertEqual(min_licenses, 25)
+ flat_discount, flat_discounted_months = self.billing_session.get_flat_discount_info()
+ self.assertEqual(flat_discounted_months, 12)
+
+ self.assert_in_success_response(
+ [
+ "Minimum purchase for",
+ f"{min_licenses} licenses",
+ "Add card",
+ "Purchase Zulip Business",
+ ],
+ result,
+ )
self.assertFalse(Customer.objects.exists())
self.assertFalse(CustomerPlan.objects.exists())
@@ -5693,10 +5711,10 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
for substring in [
"Zulip Business",
"Number of licenses",
- f"{realm_user_count} (managed automatically)",
+ f"{min_licenses} (managed automatically)",
"January 2, 2013",
"Your plan will automatically renew on",
- f"${80 * realm_user_count:,.2f}",
+ f"${80 * min_licenses:,.2f}",
"Visa ending in 4242",
"Update card",
]:
@@ -5707,7 +5725,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
self.assertEqual(LicenseLedger.objects.count(), 1)
with time_machine.travel(self.now + timedelta(days=2), tick=False):
- for count in range(4, 14):
+ for count in range(realm_user_count, min_licenses + 10):
do_create_user(
f"email {count}",
f"password {count}",
@@ -5722,11 +5740,140 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
self.assertEqual(
RemoteRealmAuditLog.objects.count(),
- audit_log_count + 10,
+ min_licenses + 10 - realm_user_count + audit_log_count,
)
latest_ledger = LicenseLedger.objects.last()
assert latest_ledger is not None
- self.assertEqual(latest_ledger.licenses, realm_user_count + 10)
+ self.assertEqual(latest_ledger.licenses, min_licenses + 10)
+
+ with time_machine.travel(self.now + timedelta(days=1), tick=False):
+ response = self.client_get(
+ f"{self.billing_session.billing_base_url}/billing/", subdomain="selfhosting"
+ )
+
+ self.assertEqual(latest_ledger.licenses, 35)
+ for substring in [
+ "Zulip Business",
+ "Number of licenses",
+ f"{latest_ledger.licenses} (managed automatically)",
+ "January 2, 2013",
+ "Your plan will automatically renew on",
+ f"${80 * latest_ledger.licenses:,.2f}",
+ "Visa ending in 4242",
+ "Update card",
+ ]:
+ self.assert_in_response(substring, response)
+
+ @responses.activate
+ @mock_stripe()
+ def test_upgrade_user_to_monthly_basic_plan(self, *mocks: Mock) -> None:
+ self.login("hamlet")
+ hamlet = self.example_user("hamlet")
+
+ self.add_mock_response()
+ realm_user_count = UserProfile.objects.filter(
+ realm=hamlet.realm, is_bot=False, is_active=True
+ ).count()
+ self.assertEqual(realm_user_count, 11)
+
+ with time_machine.travel(self.now, tick=False):
+ send_server_data_to_push_bouncer(consider_usage_statistics=False)
+
+ result = self.execute_remote_billing_authentication_flow(hamlet)
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(result["Location"], f"{self.billing_session.billing_base_url}/plans/")
+
+ # upgrade to basic plan
+ with time_machine.travel(self.now, tick=False):
+ result = self.client_get(
+ f"{self.billing_session.billing_base_url}/upgrade/?tier={CustomerPlan.TIER_SELF_HOSTED_BASIC}",
+ subdomain="selfhosting",
+ )
+ self.assertEqual(result.status_code, 200)
+
+ min_licenses = self.billing_session.min_licenses_for_plan(
+ CustomerPlan.TIER_SELF_HOSTED_BASIC
+ )
+ self.assertEqual(min_licenses, 10)
+ flat_discount, flat_discounted_months = self.billing_session.get_flat_discount_info()
+ self.assertEqual(flat_discounted_months, 12)
+
+ self.assert_in_success_response(
+ [f"{realm_user_count}", "Add card", "Purchase Zulip Basic"], result
+ )
+
+ self.assertFalse(Customer.objects.exists())
+ self.assertFalse(CustomerPlan.objects.exists())
+ self.assertFalse(LicenseLedger.objects.exists())
+
+ with time_machine.travel(self.now, tick=False):
+ stripe_customer = self.add_card_and_upgrade(
+ tier=CustomerPlan.TIER_SELF_HOSTED_BASIC, schedule="monthly"
+ )
+
+ customer = Customer.objects.get(stripe_customer_id=stripe_customer.id)
+ plan = CustomerPlan.objects.get(customer=customer)
+ LicenseLedger.objects.get(plan=plan)
+
+ with time_machine.travel(self.now + timedelta(days=1), tick=False):
+ response = self.client_get(
+ f"{self.billing_session.billing_base_url}/billing/", subdomain="selfhosting"
+ )
+ for substring in [
+ "Zulip Basic",
+ "Number of licenses",
+ f"{realm_user_count} (managed automatically)",
+ "February 2, 2012",
+ "Your plan will automatically renew on",
+ f"${3.5 * realm_user_count - flat_discount // 100 * 1:,.2f}",
+ "Visa ending in 4242",
+ "Update card",
+ ]:
+ self.assert_in_response(substring, response)
+
+ # Verify that change in user count updates LicenseLedger.
+ audit_log_count = RemoteRealmAuditLog.objects.count()
+ self.assertEqual(LicenseLedger.objects.count(), 1)
+
+ with time_machine.travel(self.now + timedelta(days=2), tick=False):
+ for count in range(realm_user_count, min_licenses + 10):
+ do_create_user(
+ f"email {count}",
+ f"password {count}",
+ hamlet.realm,
+ "name",
+ role=UserProfile.ROLE_MEMBER,
+ acting_user=None,
+ )
+
+ with time_machine.travel(self.now + timedelta(days=3), tick=False):
+ send_server_data_to_push_bouncer(consider_usage_statistics=False)
+
+ self.assertEqual(
+ RemoteRealmAuditLog.objects.count(),
+ min_licenses + 10 - realm_user_count + audit_log_count,
+ )
+ latest_ledger = LicenseLedger.objects.last()
+ assert latest_ledger is not None
+ self.assertEqual(latest_ledger.licenses, min_licenses + 10)
+
+ with time_machine.travel(self.now + timedelta(days=1), tick=False):
+ response = self.client_get(
+ f"{self.billing_session.billing_base_url}/billing/", subdomain="selfhosting"
+ )
+
+ self.assertEqual(latest_ledger.licenses, 20)
+ for substring in [
+ "Zulip Basic",
+ "Number of licenses",
+ f"{latest_ledger.licenses} (managed automatically)",
+ "February 2, 2012",
+ "Your plan will automatically renew on",
+ f"${3.5 * latest_ledger.licenses - flat_discount // 100 * 1:,.2f}",
+ "Visa ending in 4242",
+ "Update card",
+ ]:
+ self.assert_in_response(substring, response)
@responses.activate
def test_request_sponsorship(self) -> None:
@@ -5918,10 +6065,10 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
for substring in [
"Zulip Business",
"Number of licenses",
- f"{server_user_count} (managed automatically)",
+ f"{25} (managed automatically)",
"Your plan will automatically renew on",
"January 2, 2013",
- f"${80 * server_user_count:,.2f}",
+ f"${80 * 25:,.2f}",
"Visa ending in 4242",
"Update card",
]:
@@ -6151,8 +6298,6 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
self.assertEqual(new_customer_plan.tier, CustomerPlan.TIER_SELF_HOSTED_BUSINESS)
self.assertEqual(new_customer_plan.billing_cycle_anchor, end_date)
- server_user_count = UserProfile.objects.filter(is_bot=False, is_active=True).count()
-
# Visit billing page
with time_machine.travel(self.now, tick=False):
response = self.client_get(f"{billing_base_url}/billing/", subdomain="selfhosting")
@@ -6160,7 +6305,8 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
"(legacy plan)",
f"This is a legacy plan that ends on {end_date.strftime('%B %d, %Y')}",
f"Your plan will automatically upgrade to Zulip Business on {end_date.strftime('%B %d, %Y')}",
- f"Expected charge: ${80 * server_user_count:,.2f} ",
+ "Expected next charge",
+ f"${80 * 25 - 20 * 12:,.2f}",
"Visa ending in 4242",
"Update card",
]:
@@ -6189,3 +6335,114 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
m.output[1],
f"INFO:corporate.stripe:Change plan status: Customer.id: {customer.id}, CustomerPlan.id: {customer_plan.id}, status: {CustomerPlan.ACTIVE}",
)
+
+ @responses.activate
+ @mock_stripe()
+ def test_upgrade_user_to_monthly_basic_plan(self, *mocks: Mock) -> None:
+ self.login("hamlet")
+ hamlet = self.example_user("hamlet")
+
+ self.add_mock_response()
+ realm_user_count = UserProfile.objects.filter(is_bot=False, is_active=True).count()
+ self.assertEqual(realm_user_count, 18)
+
+ with time_machine.travel(self.now, tick=False):
+ send_server_data_to_push_bouncer(consider_usage_statistics=False)
+
+ result = self.execute_remote_billing_authentication_flow(
+ hamlet.delivery_email, hamlet.full_name
+ )
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(result["Location"], f"{self.billing_session.billing_base_url}/plans/")
+
+ # upgrade to basic plan
+ with time_machine.travel(self.now, tick=False):
+ result = self.client_get(
+ f"{self.billing_session.billing_base_url}/upgrade/?tier={CustomerPlan.TIER_SELF_HOSTED_BASIC}",
+ subdomain="selfhosting",
+ )
+ self.assertEqual(result.status_code, 200)
+
+ min_licenses = self.billing_session.min_licenses_for_plan(
+ CustomerPlan.TIER_SELF_HOSTED_BASIC
+ )
+ self.assertEqual(min_licenses, 10)
+ flat_discount, flat_discounted_months = self.billing_session.get_flat_discount_info()
+ self.assertEqual(flat_discounted_months, 12)
+
+ self.assert_in_success_response(
+ [f"{min_licenses}", "Add card", "Purchase Zulip Basic"], result
+ )
+
+ self.assertFalse(Customer.objects.exists())
+ self.assertFalse(CustomerPlan.objects.exists())
+ self.assertFalse(LicenseLedger.objects.exists())
+
+ with time_machine.travel(self.now, tick=False):
+ stripe_customer = self.add_card_and_upgrade(
+ tier=CustomerPlan.TIER_SELF_HOSTED_BASIC, schedule="monthly"
+ )
+
+ customer = Customer.objects.get(stripe_customer_id=stripe_customer.id)
+ plan = CustomerPlan.objects.get(customer=customer)
+ LicenseLedger.objects.get(plan=plan)
+
+ with time_machine.travel(self.now + timedelta(days=1), tick=False):
+ response = self.client_get(
+ f"{self.billing_session.billing_base_url}/billing/", subdomain="selfhosting"
+ )
+ for substring in [
+ "Zulip Basic",
+ "Number of licenses",
+ f"{realm_user_count} (managed automatically)",
+ "February 2, 2012",
+ "Your plan will automatically renew on",
+ f"${3.5 * realm_user_count - flat_discount // 100 * 1:,.2f}",
+ "Visa ending in 4242",
+ "Update card",
+ ]:
+ self.assert_in_response(substring, response)
+
+ # Verify that change in user count updates LicenseLedger.
+ audit_log_count = RemoteRealmAuditLog.objects.count()
+ self.assertEqual(LicenseLedger.objects.count(), 1)
+
+ with time_machine.travel(self.now + timedelta(days=2), tick=False):
+ for count in range(realm_user_count, min_licenses + 10):
+ do_create_user(
+ f"email {count}",
+ f"password {count}",
+ hamlet.realm,
+ "name",
+ role=UserProfile.ROLE_MEMBER,
+ acting_user=None,
+ )
+
+ with time_machine.travel(self.now + timedelta(days=3), tick=False):
+ send_server_data_to_push_bouncer(consider_usage_statistics=False)
+
+ self.assertEqual(
+ RemoteRealmAuditLog.objects.count(),
+ min_licenses + 10 - realm_user_count + audit_log_count,
+ )
+ latest_ledger = LicenseLedger.objects.last()
+ assert latest_ledger is not None
+ self.assertEqual(latest_ledger.licenses, min_licenses + 10)
+
+ with time_machine.travel(self.now + timedelta(days=1), tick=False):
+ response = self.client_get(
+ f"{self.billing_session.billing_base_url}/billing/", subdomain="selfhosting"
+ )
+
+ self.assertEqual(latest_ledger.licenses, 20)
+ for substring in [
+ "Zulip Basic",
+ "Number of licenses",
+ f"{latest_ledger.licenses} (managed automatically)",
+ "February 2, 2012",
+ "Your plan will automatically renew on",
+ f"${3.5 * latest_ledger.licenses - flat_discount // 100 * 1:,.2f}",
+ "Visa ending in 4242",
+ "Update card",
+ ]:
+ self.assert_in_response(substring, response)
diff --git a/templates/corporate/billing.html b/templates/corporate/billing.html
index bb6a70af6e..f384af9826 100644
--- a/templates/corporate/billing.html
+++ b/templates/corporate/billing.html
@@ -237,25 +237,34 @@
Your next invoice is due on {{ renewal_date }} .
{% endif %}
- Expected charge: ${{ renewal_amount }}
- {% if not fixed_price %}
- (${{ price_per_license }} x {{ licenses_at_next_renewal }} {{ 'user' if licenses_at_next_renewal == 1 else 'users' }} x
- {% if switch_to_annual_at_end_of_cycle %}
- 12 months
- {%- elif switch_to_monthly_at_end_of_cycle %}
- 1 month
- {%- else %}
- {{ "1 month" if billing_frequency == "Monthly" else "12 months" }}
- {%- endif -%})
- {% if discount_percent %}
-
- Includes: {{ discount_percent }}% discount
+
+
Expected next charge
+ {% if not fixed_price %}
+
+ {% if using_min_licenses_for_plan %}
+ Minimum purchase for this plan: {{ min_licenses_for_plan }} licenses
+
+ {% endif %}
+ ${{ pre_discount_renewal_cents }} (${{ price_per_license }} x {{ licenses_at_next_renewal }} {{ 'user' if licenses_at_next_renewal == 1 else 'users' }} x
+ {% if switch_to_annual_at_end_of_cycle %}
+ 12 months
+ {%- elif switch_to_monthly_at_end_of_cycle %}
+ 1 month
+ {%- else %}
+ {{ "1 month" if billing_frequency == "Monthly" else "12 months" }}
+ {%- endif -%})
+ {% if discounted_months_left != 0 %}
+
+ Discount: ${{ flat_discount }}/month off ({{ discounted_months_left }} {{ "month" if discounted_months_left == 1 else "months" }} remaining)
+ {% endif %}
+ {% if discount_percent %}
+
+ Includes: {{ discount_percent }}% discount
+ {% endif %}
+
{% endif %}
- {% if using_min_licenses_for_plan %}
-
-
Minimum purchase for this plan: {{ min_licenses_for_plan }} licenses
- {% endif %}
- {% endif %}
+
+ ${{ renewal_amount }}
{% endif %}
{% else %}
Your plan ends on {{ renewal_date }} , and does not renew.
@@ -498,12 +507,12 @@
- Your organization will be not be upgraded to Zulip Business
+ Your organization will be not be upgraded to {{ legacy_remote_server_next_plan_name }}
on {{ remote_server_legacy_plan_end_date }}. If your organization has more than
10 users at that time, you will lose access to the
Mobile Push Notification Service .
You will also not receive the other benefits
- of the Zulip Business plan. Are you sure you want to continue?
+ of the {{ legacy_remote_server_next_plan_name }} plan. Are you sure you want to continue?