stripe: Fix legacy plans not being invoiced.

Earlier, in 'migrate_customer_to_legacy_plan`, we set
'next_invoice_date' to None for legacy plans.
This will result in legacy plans not getting invoiced.
We need to invoice legacy plans on their end date to
either downgrade them or switch to a new plan.

We set next_invoice_date for legacy plans to end_date.
This commit is contained in:
Prakhar Pratyush 2024-01-22 18:50:49 +05:30 committed by Tim Abbott
parent e4258b56d5
commit 026eb37c28
3 changed files with 49 additions and 7 deletions

View File

@ -854,7 +854,6 @@ class BillingSession(ABC):
customer=customer, customer=customer,
tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY, tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY,
status=status, status=status,
next_invoice_date=None,
).first() ).first()
def get_formatted_remote_server_legacy_plan_end_date( def get_formatted_remote_server_legacy_plan_end_date(
@ -2418,7 +2417,10 @@ class BillingSession(ABC):
raise NotImplementedError( raise NotImplementedError(
"Plan with invoicing_status==STARTED needs manual resolution." "Plan with invoicing_status==STARTED needs manual resolution."
) )
if not plan.customer.stripe_customer_id: if (
plan.tier != CustomerPlan.TIER_SELF_HOSTED_LEGACY
and not plan.customer.stripe_customer_id
):
raise BillingError( raise BillingError(
f"Customer has a paid plan without a Stripe customer ID: {plan.customer!s}" f"Customer has a paid plan without a Stripe customer ID: {plan.customer!s}"
) )
@ -2483,6 +2485,7 @@ class BillingSession(ABC):
plan.invoiced_through = ledger_entry plan.invoiced_through = ledger_entry
plan.invoicing_status = CustomerPlan.INVOICING_STATUS_STARTED plan.invoicing_status = CustomerPlan.INVOICING_STATUS_STARTED
plan.save(update_fields=["invoicing_status", "invoiced_through"]) plan.save(update_fields=["invoicing_status", "invoiced_through"])
assert plan.customer.stripe_customer_id is not None
stripe.InvoiceItem.create( stripe.InvoiceItem.create(
currency="usd", currency="usd",
customer=plan.customer.stripe_customer_id, customer=plan.customer.stripe_customer_id,
@ -2918,10 +2921,10 @@ class BillingSession(ABC):
"tier": CustomerPlan.TIER_SELF_HOSTED_LEGACY, "tier": CustomerPlan.TIER_SELF_HOSTED_LEGACY,
# End when the new plan starts. # End when the new plan starts.
"end_date": end_date, "end_date": end_date,
"next_invoice_date": end_date,
# The primary mechanism for preventing charges under this # The primary mechanism for preventing charges under this
# plan is setting a null `next_invoice_date`, but setting # plan is setting 'invoiced_through' to last ledger_entry below,
# a 0 price is useful defense in depth here. # but setting a 0 price is useful defense in depth here.
"next_invoice_date": None,
"price_per_license": 0, "price_per_license": 0,
"billing_schedule": CustomerPlan.BILLING_SCHEDULE_ANNUAL, "billing_schedule": CustomerPlan.BILLING_SCHEDULE_ANNUAL,
"automanage_licenses": True, "automanage_licenses": True,
@ -4433,7 +4436,7 @@ def get_plan_renewal_or_end_date(plan: CustomerPlan, event_time: datetime) -> da
def invoice_plans_as_needed(event_time: Optional[datetime] = None) -> None: def invoice_plans_as_needed(event_time: Optional[datetime] = None) -> None:
if event_time is None: # nocoverage if event_time is None:
event_time = timezone_now() event_time = timezone_now()
for plan in CustomerPlan.objects.filter(next_invoice_date__lte=event_time): for plan in CustomerPlan.objects.filter(next_invoice_date__lte=event_time):
remote_server: Optional[RemoteZulipServer] = None remote_server: Optional[RemoteZulipServer] = None

View File

@ -0,0 +1,28 @@
# Generated by Django 4.2.8 on 2024-01-25 05:49
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
from django.db.models import F
def update_legacy_plan_next_invoice_date(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
CustomerPlan = apps.get_model("corporate", "CustomerPlan")
CustomerPlan.TIER_SELF_HOSTED_LEGACY = 101
# For legacy plans, set next_invoice_date = end_date.
CustomerPlan.objects.filter(tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY).update(
next_invoice_date=F("end_date")
)
class Migration(migrations.Migration):
dependencies = [
("corporate", "0034_customer_discount_required_tier"),
]
operations = [
migrations.RunPython(update_legacy_plan_next_invoice_date),
]

View File

@ -7359,10 +7359,21 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
assert customer is not None assert customer is not None
plan = get_current_plan_by_customer(customer) plan = get_current_plan_by_customer(customer)
assert plan is not None assert plan is not None
self.assertEqual(plan.end_date, plan_end_date)
self.assertEqual(plan.next_invoice_date, plan_end_date)
self.assertEqual(plan.status, CustomerPlan.ACTIVE)
self.assertEqual( self.assertEqual(
self.remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY self.remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY
) )
self.billing_session.make_end_of_cycle_updates_if_needed(plan, plan_end_date)
with mock.patch("stripe.Invoice.create") as invoice_create, time_machine.travel(
plan_end_date, tick=False
):
send_server_data_to_push_bouncer(consider_usage_statistics=False)
invoice_plans_as_needed()
# The legacy plan is downgraded, no invoice created.
invoice_create.assert_not_called()
plan.refresh_from_db() plan.refresh_from_db()
self.remote_server.refresh_from_db() self.remote_server.refresh_from_db()
self.assertEqual(self.remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED) self.assertEqual(self.remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED)