From caba57fe1e0df39d8e60963dafedb69b47bdbd1b Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Tue, 5 Mar 2024 05:06:57 +0000 Subject: [PATCH] stripe: Bill manually managed licenses monthly for additional licenses. Regardless of plan renewal schedule, we try to invoice all plans monthly with some exceptions like free trial and fixed price plans, which help us charge users for additional licenses used during the previous month. --- corporate/lib/stripe.py | 19 ++++--------------- corporate/tests/test_stripe.py | 29 +++++++++++------------------ 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 94e0e5c935..261f269b65 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -313,12 +313,7 @@ def next_invoice_date(plan: CustomerPlan) -> Optional[datetime]: if plan.status == CustomerPlan.ENDED: return None assert plan.next_invoice_date is not None # for mypy - months_per_period = { - CustomerPlan.BILLING_SCHEDULE_ANNUAL: 12, - CustomerPlan.BILLING_SCHEDULE_MONTHLY: 1, - }[plan.billing_schedule] - if plan.automanage_licenses: - months_per_period = 1 + months_per_period = 1 periods = 1 dt = plan.billing_cycle_anchor while dt <= plan.next_invoice_date: @@ -1657,7 +1652,6 @@ class BillingSession(ABC): price_per_license, ) = compute_plan_parameters( plan_tier, - automanage_licenses, billing_schedule, discount_for_plan, free_trial, @@ -1923,7 +1917,6 @@ class BillingSession(ABC): discount_for_current_plan = plan.discount _, _, _, price_per_license = compute_plan_parameters( tier=plan.tier, - automanage_licenses=plan.automanage_licenses, billing_schedule=schedule, discount=discount_for_current_plan, ) @@ -2069,7 +2062,6 @@ class BillingSession(ABC): discount_for_current_plan = plan.discount _, _, _, price_per_license = compute_plan_parameters( tier=plan.tier, - automanage_licenses=plan.automanage_licenses, billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL, discount=discount_for_current_plan, ) @@ -2118,7 +2110,6 @@ class BillingSession(ABC): discount_for_current_plan = plan.discount _, _, _, price_per_license = compute_plan_parameters( tier=plan.tier, - automanage_licenses=plan.automanage_licenses, billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY, discount=discount_for_current_plan, ) @@ -2469,7 +2460,6 @@ class BillingSession(ABC): if free_trial_days is not None: _, _, free_trial_end, _ = compute_plan_parameters( tier, - False, CustomerPlan.BILLING_SCHEDULE_ANNUAL, None, True, @@ -4716,7 +4706,6 @@ def get_price_per_license( def compute_plan_parameters( tier: int, - automanage_licenses: bool, billing_schedule: int, discount: Optional[Decimal], free_trial: bool = False, @@ -4739,9 +4728,9 @@ def compute_plan_parameters( price_per_license = get_price_per_license(tier, billing_schedule, discount) - next_invoice_date = period_end - if automanage_licenses: - next_invoice_date = add_months(billing_cycle_anchor, 1) + # `next_invoice_date` is the date when we check if there are any invoices that need to be generated. + # It is always the next month regardless of the billing schedule / billing modality. + next_invoice_date = add_months(billing_cycle_anchor, 1) if free_trial: period_end = billing_cycle_anchor + timedelta( days=assert_is_not_none(get_free_trial_days(is_self_hosted_billing, tier)) diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index bdd2c17f75..5d70ab3655 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -1039,7 +1039,7 @@ class StripeTest(StripeTestCase): billing_cycle_anchor=self.now, billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL, invoiced_through=LicenseLedger.objects.first(), - next_invoice_date=self.next_year, + next_invoice_date=self.next_month, tier=CustomerPlan.TIER_CLOUD_STANDARD, status=CustomerPlan.ACTIVE, ) @@ -1438,7 +1438,7 @@ class StripeTest(StripeTestCase): customer_plan.refresh_from_db() realm.refresh_from_db() self.assertEqual(customer_plan.status, CustomerPlan.ACTIVE) - self.assertEqual(customer_plan.next_invoice_date, add_months(free_trial_end_date, 12)) + self.assertEqual(customer_plan.next_invoice_date, add_months(free_trial_end_date, 1)) self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD) [invoice] = iter(stripe.Invoice.list(customer=stripe_customer.id)) invoice_params = { @@ -2719,7 +2719,7 @@ class StripeTest(StripeTestCase): annual_plan.refresh_from_db() self.assertEqual(annual_plan.invoiced_through, annual_ledger_entries[0]) - self.assertEqual(annual_plan.next_invoice_date, add_months(self.next_month, 12)) + self.assertEqual(annual_plan.next_invoice_date, add_months(self.next_month, 1)) self.assertEqual(annual_plan.invoicing_status, CustomerPlan.INVOICING_STATUS_DONE) assert customer.stripe_customer_id @@ -2743,7 +2743,8 @@ class StripeTest(StripeTestCase): with patch("corporate.lib.stripe.BillingSession.invoice_plan") as m: invoice_plans_as_needed(add_months(self.now, 2)) - m.assert_not_called() + # Even annual plans get invoiced monthly for additional licenses. + m.assert_called_once() invoice_plans_as_needed(add_months(self.now, 13)) @@ -4542,46 +4543,42 @@ class BillingHelpersTest(ZulipTestCase): ( ( CustomerPlan.TIER_CLOUD_STANDARD, - True, CustomerPlan.BILLING_SCHEDULE_ANNUAL, None, ), (anchor, month_later, year_later, 8000), ), ( - (CustomerPlan.TIER_CLOUD_STANDARD, True, CustomerPlan.BILLING_SCHEDULE_ANNUAL, 85), + (CustomerPlan.TIER_CLOUD_STANDARD, CustomerPlan.BILLING_SCHEDULE_ANNUAL, 85), (anchor, month_later, year_later, 1200), ), ( ( CustomerPlan.TIER_CLOUD_STANDARD, - True, CustomerPlan.BILLING_SCHEDULE_MONTHLY, None, ), (anchor, month_later, month_later, 800), ), ( - (CustomerPlan.TIER_CLOUD_STANDARD, True, CustomerPlan.BILLING_SCHEDULE_MONTHLY, 85), + (CustomerPlan.TIER_CLOUD_STANDARD, CustomerPlan.BILLING_SCHEDULE_MONTHLY, 85), (anchor, month_later, month_later, 120), ), ( ( CustomerPlan.TIER_CLOUD_STANDARD, - False, CustomerPlan.BILLING_SCHEDULE_ANNUAL, None, ), - (anchor, year_later, year_later, 8000), + (anchor, month_later, year_later, 8000), ), ( - (CustomerPlan.TIER_CLOUD_STANDARD, False, CustomerPlan.BILLING_SCHEDULE_ANNUAL, 85), - (anchor, year_later, year_later, 1200), + (CustomerPlan.TIER_CLOUD_STANDARD, CustomerPlan.BILLING_SCHEDULE_ANNUAL, 85), + (anchor, month_later, year_later, 1200), ), ( ( CustomerPlan.TIER_CLOUD_STANDARD, - False, CustomerPlan.BILLING_SCHEDULE_MONTHLY, None, ), @@ -4590,7 +4587,6 @@ class BillingHelpersTest(ZulipTestCase): ( ( CustomerPlan.TIER_CLOUD_STANDARD, - False, CustomerPlan.BILLING_SCHEDULE_MONTHLY, 85, ), @@ -4600,7 +4596,6 @@ class BillingHelpersTest(ZulipTestCase): ( ( CustomerPlan.TIER_CLOUD_STANDARD, - False, CustomerPlan.BILLING_SCHEDULE_MONTHLY, 87.25, ), @@ -4610,7 +4605,6 @@ class BillingHelpersTest(ZulipTestCase): ( ( CustomerPlan.TIER_CLOUD_STANDARD, - False, CustomerPlan.BILLING_SCHEDULE_MONTHLY, 87.15, ), @@ -4618,10 +4612,9 @@ class BillingHelpersTest(ZulipTestCase): ), ] with time_machine.travel(anchor, tick=False): - for (tier, automanage_licenses, billing_schedule, discount), output in test_cases: + for (tier, billing_schedule, discount), output in test_cases: output_ = compute_plan_parameters( tier, - automanage_licenses, billing_schedule, None if discount is None else Decimal(discount), )