mirror of https://github.com/zulip/zulip.git
stripe: Add cron-based plan invoicing to remote realm billing system.
This commit is contained in:
parent
6088186223
commit
11908c4c2e
|
@ -2478,7 +2478,8 @@ class BillingSession(ABC):
|
|||
stripe.Invoice.finalize_invoice(stripe_invoice)
|
||||
|
||||
plan.next_invoice_date = next_invoice_date(plan)
|
||||
plan.save(update_fields=["next_invoice_date"])
|
||||
plan.invoice_overdue_email_sent = False
|
||||
plan.save(update_fields=["next_invoice_date", "invoice_overdue_email_sent"])
|
||||
|
||||
def do_change_plan_to_new_tier(self, new_plan_tier: int) -> str:
|
||||
customer = self.get_customer()
|
||||
|
@ -4341,10 +4342,40 @@ def get_plan_renewal_or_end_date(plan: CustomerPlan, event_time: datetime) -> da
|
|||
def invoice_plans_as_needed(event_time: Optional[datetime] = None) -> None:
|
||||
if event_time is None: # nocoverage
|
||||
event_time = timezone_now()
|
||||
# TODO: Add RemoteRealmBillingSession and RemoteServerBillingSession cases.
|
||||
# TODO: Add RemoteServerBillingSession cases.
|
||||
for plan in CustomerPlan.objects.filter(next_invoice_date__lte=event_time):
|
||||
if plan.customer.realm is not None:
|
||||
RealmBillingSession(realm=plan.customer.realm).invoice_plan(plan, event_time)
|
||||
elif plan.customer.remote_realm is not None:
|
||||
remote_realm = plan.customer.remote_realm
|
||||
remote_server = remote_realm.server
|
||||
billing_session = RemoteRealmBillingSession(remote_realm=remote_realm)
|
||||
|
||||
assert remote_server.last_audit_log_update is not None
|
||||
assert plan.next_invoice_date is not None
|
||||
if plan.next_invoice_date > remote_server.last_audit_log_update:
|
||||
if (
|
||||
plan.next_invoice_date - remote_server.last_audit_log_update
|
||||
>= timedelta(days=1)
|
||||
and not plan.invoice_overdue_email_sent
|
||||
):
|
||||
context = {
|
||||
"support_url": billing_session.support_url(),
|
||||
"last_audit_log_update": remote_server.last_audit_log_update.strftime(
|
||||
"%Y-%m-%d"
|
||||
),
|
||||
}
|
||||
send_email(
|
||||
"zerver/emails/invoice_overdue",
|
||||
to_emails=[BILLING_SUPPORT_EMAIL],
|
||||
from_address=FromAddress.tokenized_no_reply_address(),
|
||||
context=context,
|
||||
)
|
||||
plan.invoice_overdue_email_sent = True
|
||||
plan.save(update_fields=["invoice_overdue_email_sent"])
|
||||
continue
|
||||
|
||||
billing_session.invoice_plan(plan, event_time)
|
||||
# TODO: Assert that we never invoice legacy plans.
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 4.2.8 on 2024-01-10 07:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("corporate", "0032_customer_minimum_licenses"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="customerplan",
|
||||
name="invoice_overdue_email_sent",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -249,6 +249,11 @@ class CustomerPlan(models.Model):
|
|||
# next_invoice_date.
|
||||
next_invoice_date = models.DateTimeField(db_index=True, null=True)
|
||||
|
||||
# Flag to track if an email has been sent to Zulip team for
|
||||
# invoice overdue by >= one day. Helps to send an email only once
|
||||
# and not every time when cron run.
|
||||
invoice_overdue_email_sent = models.BooleanField(default=False)
|
||||
|
||||
# On next_invoice_date, we go through ledger entries that were
|
||||
# created after invoiced_through and process them by generating
|
||||
# invoices for any additional users and/or plan renewal. Once the
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -6378,6 +6378,105 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
|
|||
for key, value in invoice_item_params.items():
|
||||
self.assertEqual(invoice_item2[key], value)
|
||||
|
||||
@responses.activate
|
||||
@mock_stripe()
|
||||
def test_invoice_plans_as_needed(self, *mocks: Mock) -> None:
|
||||
self.login("hamlet")
|
||||
hamlet = self.example_user("hamlet")
|
||||
|
||||
self.add_mock_response()
|
||||
with time_machine.travel(self.now, tick=False):
|
||||
send_server_data_to_push_bouncer(consider_usage_statistics=False)
|
||||
|
||||
self.execute_remote_billing_authentication_flow(hamlet)
|
||||
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)
|
||||
assert plan.customer.remote_realm is not None
|
||||
self.assertEqual(plan.next_invoice_date, self.next_month)
|
||||
|
||||
with time_machine.travel(self.now + timedelta(days=2), tick=False):
|
||||
for count in range(5):
|
||||
do_create_user(
|
||||
f"email - {count}",
|
||||
f"password {count}",
|
||||
hamlet.realm,
|
||||
"name",
|
||||
role=UserProfile.ROLE_MEMBER,
|
||||
acting_user=None,
|
||||
)
|
||||
|
||||
# Data upload was 25 days before the invoice date.
|
||||
last_audit_log_update = self.now + timedelta(days=5)
|
||||
with time_machine.travel(last_audit_log_update, tick=False):
|
||||
send_server_data_to_push_bouncer(consider_usage_statistics=False)
|
||||
invoice_plans_as_needed(self.next_month)
|
||||
plan.refresh_from_db()
|
||||
self.assertEqual(plan.next_invoice_date, self.next_month)
|
||||
self.assertTrue(plan.invoice_overdue_email_sent)
|
||||
|
||||
from django.core.mail import outbox
|
||||
|
||||
messages_count = len(outbox)
|
||||
message = outbox[-1]
|
||||
self.assert_length(message.to, 1)
|
||||
self.assertEqual(message.to[0], "sales@zulip.com")
|
||||
self.assertEqual(message.subject, "Invoice overdue due to stale data")
|
||||
self.assertIn(
|
||||
f"Support URL: {self.billing_session.support_url()}",
|
||||
message.body,
|
||||
)
|
||||
self.assertIn(
|
||||
f"Last data upload: {last_audit_log_update.strftime('%Y-%m-%d')}", message.body
|
||||
)
|
||||
|
||||
# Cron runs again, don't send another email to Zulip team.
|
||||
invoice_plans_as_needed(self.next_month + timedelta(days=1))
|
||||
self.assert_length(outbox, messages_count)
|
||||
|
||||
# Ledger is up-to-date. Plan invoiced.
|
||||
with time_machine.travel(self.next_month, tick=False):
|
||||
send_server_data_to_push_bouncer(consider_usage_statistics=False)
|
||||
invoice_plans_as_needed(self.next_month)
|
||||
plan.refresh_from_db()
|
||||
self.assertEqual(plan.next_invoice_date, add_months(self.next_month, 1))
|
||||
self.assertFalse(plan.invoice_overdue_email_sent)
|
||||
|
||||
assert customer.stripe_customer_id
|
||||
[invoice0, invoice1] = iter(stripe.Invoice.list(customer=customer.stripe_customer_id))
|
||||
|
||||
[invoice_item0, invoice_item1, invoice_item2] = iter(invoice0.lines)
|
||||
invoice_item_params = {
|
||||
"amount": 16 * 3.5 * 100,
|
||||
"description": "Zulip Basic - renewal",
|
||||
"quantity": 16,
|
||||
"period": {
|
||||
"start": datetime_to_timestamp(self.next_month),
|
||||
"end": datetime_to_timestamp(add_months(self.next_month, 1)),
|
||||
},
|
||||
}
|
||||
for key, value in invoice_item_params.items():
|
||||
self.assertEqual(invoice_item1[key], value)
|
||||
|
||||
invoice_item_params = {
|
||||
"description": "Additional license (Jan 4, 2012 - Feb 2, 2012)",
|
||||
"quantity": 5,
|
||||
"period": {
|
||||
"start": datetime_to_timestamp(self.now + timedelta(days=2)),
|
||||
"end": datetime_to_timestamp(self.next_month),
|
||||
},
|
||||
}
|
||||
for key, value in invoice_item_params.items():
|
||||
self.assertEqual(invoice_item2[key], value)
|
||||
|
||||
# Verify Zulip team receives mail for the next cycle.
|
||||
invoice_plans_as_needed(add_months(self.next_month, 1))
|
||||
self.assert_length(outbox, messages_count + 1)
|
||||
|
||||
|
||||
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
|
||||
class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "zerver/emails/email_base_default.html" %}
|
||||
|
||||
{% block content %}
|
||||
<b>Support URL</b>: <a href="{{ support_url }}">{{ support_url }}</a>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<b>Last data upload</b>: {{ last_audit_log_update }}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1 @@
|
|||
Invoice overdue due to stale data
|
|
@ -0,0 +1,3 @@
|
|||
Support URL: {{ support_url }}
|
||||
|
||||
Last data upload: {{ last_audit_log_update }}
|
Loading…
Reference in New Issue