stripe: Add page to show list of past customer invoices.

This commit is contained in:
Aman Agrawal 2024-02-01 15:24:05 +00:00 committed by Tim Abbott
parent d8ec141de2
commit de33aa4b7b
85 changed files with 137 additions and 1 deletions

View File

@ -753,6 +753,37 @@ class BillingSession(ABC):
def org_name(self) -> str:
pass
def get_past_invoices_session_url(self) -> str:
headline = "List of past invoices"
customer = self.get_customer()
assert customer is not None and customer.stripe_customer_id is not None
# Check if customer has any $0 invoices.
if stripe.Invoice.list(
customer=customer.stripe_customer_id,
limit=1,
status="paid",
total=0,
).data: # nocoverage
# These are payment for upgrades which were paid directly by the customer and then we
# created an invoice for them resulting in `$0` invoices since there was no amount due.
headline += " ($0 invoices include payment)"
configuration = stripe.billing_portal.Configuration.create(
business_profile={
"headline": headline,
},
features={
"invoice_history": {"enabled": True},
},
)
return stripe.billing_portal.Session.create(
customer=customer.stripe_customer_id,
configuration=configuration.id,
return_url=f"{self.billing_session_url}/billing/",
).url
def get_data_for_stripe_payment_intent(
self,
customer: Customer,

View File

@ -295,6 +295,8 @@ def normalize_fixture_data(
MOCKED_STRIPE_FUNCTION_NAMES = [
f"stripe.{name}"
for name in [
"billing_portal.Configuration.create",
"billing_portal.Session.create",
"checkout.Session.create",
"checkout.Session.list",
"Charge.create",
@ -785,6 +787,15 @@ class StripeTest(StripeTestCase):
response = self.client_get("/upgrade/", follow=True)
self.assertEqual(response.status_code, 404)
@mock_stripe()
def test_get_past_invoices_session_url(self, *mocks: Mock) -> None:
user = self.example_user("hamlet")
self.login_user(user)
self.add_card_and_upgrade(user)
response = self.client_get("/invoices/")
self.assertEqual(response.status_code, 302)
self.assertTrue(response["Location"].startswith("https://billing.stripe.com"))
@mock_stripe(tested_timestamp_fields=["created"])
def test_upgrade_by_card(self, *mocks: Mock) -> None:
user = self.example_user("hamlet")
@ -1944,6 +1955,10 @@ class StripeTest(StripeTestCase):
response,
)
response = self.client_get("/invoices/")
self.assertEqual(response.status_code, 302)
self.assertEqual(response["Location"], "/billing/")
user.realm.plan_type = Realm.PLAN_TYPE_PLUS
user.realm.save()
response = self.client_get("/sponsorship/")
@ -5927,6 +5942,25 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
]:
self.assert_in_response(substring, response)
@responses.activate
@mock_stripe()
def test_get_past_invoices_session_url_for_remote_realm(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)
self.add_card_and_upgrade()
response = self.client_get(
f"{self.billing_session.billing_base_url}/invoices/", subdomain="selfhosting"
)
self.assertEqual(response.status_code, 302)
self.assertTrue(response["Location"].startswith("https://billing.stripe.com"))
@responses.activate
@mock_stripe()
def test_upgrade_user_to_basic_plan_free_trial(self, *mocks: Mock) -> None:
@ -7544,6 +7578,25 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
self.billing_session.min_licenses_for_plan(CustomerPlan.TIER_SELF_HOSTED_BASIC), 1
)
@responses.activate
@mock_stripe()
def test_get_past_invoices_session_url_for_remote_server(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.delivery_email, hamlet.full_name)
self.add_card_and_upgrade()
response = self.client_get(
f"{self.billing_session.billing_base_url}/invoices/", subdomain="selfhosting"
)
self.assertEqual(response.status_code, 302)
self.assertTrue(response["Location"].startswith("https://billing.stripe.com"))
@responses.activate
@mock_stripe()
def test_upgrade_server_to_fixed_price_monthly_basic_plan(self, *mocks: Mock) -> None:

View File

@ -30,9 +30,12 @@ from corporate.views.portico import (
apps_view,
communities_view,
hello_view,
invoices_page,
landing_view,
plans_view,
remote_realm_invoices_page,
remote_realm_plans_page,
remote_server_invoices_page,
remote_server_plans_page,
team_view,
)
@ -83,6 +86,7 @@ i18n_urlpatterns: Any = [
path("jobs/", TemplateView.as_view(template_name="corporate/jobs.html")),
# Billing
path("billing/", billing_page, name="billing_page"),
path("invoices/", invoices_page, name="invoices_page"),
path("sponsorship/", sponsorship_page, name="sponsorship_request"),
path("upgrade/", upgrade_page, name="upgrade_page"),
path("support/", support_request),
@ -281,6 +285,16 @@ urlpatterns += [
remote_server_event_status_page,
name="remote_server_event_status_page",
),
path(
"realm/<realm_uuid>/invoices/",
remote_realm_invoices_page,
name="remote_realm_invoices_page",
),
path(
"server/<server_uuid>/invoices/",
remote_server_invoices_page,
name="remote_server_invoices_page",
),
# Remote variants of above API endpoints.
path("json/realm/<realm_uuid>/billing/sponsorship", remote_realm_sponsorship),
path("json/server/<server_uuid>/billing/sponsorship", remote_server_sponsorship),

View File

@ -14,6 +14,7 @@ from corporate.lib.decorator import (
authenticated_remote_server_management_endpoint,
)
from corporate.lib.stripe import (
RealmBillingSession,
RemoteRealmBillingSession,
RemoteServerBillingSession,
get_configured_fixed_price_plan_offer,
@ -21,7 +22,7 @@ from corporate.lib.stripe import (
)
from corporate.models import CustomerPlan, get_current_plan_by_customer, get_customer_by_realm
from zerver.context_processors import get_realm_from_request, latest_info_context
from zerver.decorator import add_google_analytics
from zerver.decorator import add_google_analytics, zulip_login_required
from zerver.lib.github import (
InvalidPlatformError,
get_latest_github_release_download_link_for_platform,
@ -359,3 +360,32 @@ def communities_view(request: HttpRequest) -> HttpResponse:
"org_types": org_types,
},
)
@zulip_login_required
def invoices_page(request: HttpRequest) -> HttpResponseRedirect:
user = request.user
assert user.is_authenticated
if not user.has_billing_access:
return HttpResponseRedirect(reverse("billing_page"))
billing_session = RealmBillingSession(user=user, realm=user.realm)
list_invoices_session_url = billing_session.get_past_invoices_session_url()
return HttpResponseRedirect(list_invoices_session_url)
@authenticated_remote_realm_management_endpoint
def remote_realm_invoices_page(
request: HttpRequest, billing_session: RemoteRealmBillingSession
) -> HttpResponseRedirect:
list_invoices_session_url = billing_session.get_past_invoices_session_url()
return HttpResponseRedirect(list_invoices_session_url)
@authenticated_remote_server_management_endpoint
def remote_server_invoices_page(
request: HttpRequest, billing_session: RemoteServerBillingSession
) -> HttpResponseRedirect:
list_invoices_session_url = billing_session.get_past_invoices_session_url()
return HttpResponseRedirect(list_invoices_session_url)

View File

@ -275,6 +275,14 @@
{% endif %}
</div>
</div>
<div class="input-box billing-page-field no-validation">
<label class="inline-block label-title">Invoices</label>
<div class="not-editable-realm-field">
<a href="{{ billing_base_url }}/invoices/">
View past invoices
</a>
</div>
</div>
<div id="planchange-error" class="alert alert-danger"></div>
<div id="planchange-input-section">
{% if free_trial and not downgrade_at_end_of_free_trial %}