billing: Make various buttons on billing page work.

We pass billing_base_url to the template and use it to construct
session specific URLs. Also, add corresponding function on server
to support them.
This commit is contained in:
Aman Agrawal
2023-12-02 08:09:43 +00:00
committed by Tim Abbott
parent 7e7af6266d
commit a59245e932
7 changed files with 167 additions and 78 deletions

View File

@@ -863,7 +863,7 @@ class BillingSession(ABC):
) )
return stripe_payment_intent.id return stripe_payment_intent.id
def get_card_update_session_data_for_upgrade( def create_card_update_session_for_upgrade(
self, self,
manual_license_management: bool, manual_license_management: bool,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@@ -892,12 +892,8 @@ class BillingSession(ABC):
"stripe_session_id": stripe_session.id, "stripe_session_id": stripe_session.id,
} }
def create_stripe_checkout_session( def create_card_update_session(self) -> Dict[str, Any]:
self, metadata = self.get_metadata_for_stripe_update_card()
metadata: Dict[str, Any],
session_type: int,
) -> stripe.checkout.Session:
# Create checkout sessions for adding and updating exiting cards.
customer = self.get_customer() customer = self.get_customer()
assert customer is not None and customer.stripe_customer_id is not None assert customer is not None and customer.stripe_customer_id is not None
stripe_session = stripe.checkout.Session.create( stripe_session = stripe.checkout.Session.create(
@@ -911,9 +907,12 @@ class BillingSession(ABC):
Session.objects.create( Session.objects.create(
stripe_session_id=stripe_session.id, stripe_session_id=stripe_session.id,
customer=customer, customer=customer,
type=session_type, type=Session.CARD_UPDATE_FROM_BILLING_PAGE,
) )
return stripe_session return {
"stripe_session_url": stripe_session.url,
"stripe_session_id": stripe_session.id,
}
def attach_discount_to_customer(self, new_discount: Decimal) -> str: def attach_discount_to_customer(self, new_discount: Decimal) -> str:
customer = self.get_customer() customer = self.get_customer()

View File

@@ -9,6 +9,8 @@ from corporate.views.billing_page import (
remote_realm_billing_page, remote_realm_billing_page,
remote_server_billing_page, remote_server_billing_page,
update_plan, update_plan,
update_plan_for_remote_realm,
update_plan_for_remote_server,
) )
from corporate.views.event_status import ( from corporate.views.event_status import (
event_status, event_status,
@@ -36,7 +38,9 @@ from corporate.views.remote_billing_page import (
from corporate.views.session import ( from corporate.views.session import (
start_card_update_stripe_session, start_card_update_stripe_session,
start_card_update_stripe_session_for_realm_upgrade, start_card_update_stripe_session_for_realm_upgrade,
start_card_update_stripe_session_for_remote_realm,
start_card_update_stripe_session_for_remote_realm_upgrade, start_card_update_stripe_session_for_remote_realm_upgrade,
start_card_update_stripe_session_for_remote_server,
start_card_update_stripe_session_for_remote_server_upgrade, start_card_update_stripe_session_for_remote_server_upgrade,
) )
from corporate.views.sponsorship import ( from corporate.views.sponsorship import (
@@ -231,6 +235,14 @@ urlpatterns += [
# Remote variants of above API endpoints. # Remote variants of above API endpoints.
path("json/realm/<realm_uuid>/sponsorship", remote_realm_sponsorship), path("json/realm/<realm_uuid>/sponsorship", remote_realm_sponsorship),
path("json/server/<server_uuid>/sponsorship", remote_server_sponsorship), path("json/server/<server_uuid>/sponsorship", remote_server_sponsorship),
path(
"json/realm/<realm_uuid>/billing/session/start_card_update_session",
start_card_update_stripe_session_for_remote_realm,
),
path(
"json/server/<server_uuid>/billing/session/start_card_update_session",
start_card_update_stripe_session_for_remote_server,
),
path( path(
"json/realm/<realm_uuid>/upgrade/session/start_card_update_session", "json/realm/<realm_uuid>/upgrade/session/start_card_update_session",
start_card_update_stripe_session_for_remote_realm_upgrade, start_card_update_stripe_session_for_remote_realm_upgrade,
@@ -243,6 +255,8 @@ urlpatterns += [
path("json/server/<server_uuid>/billing/event/status", remote_server_event_status), path("json/server/<server_uuid>/billing/event/status", remote_server_event_status),
path("json/realm/<realm_uuid>/billing/upgrade", remote_realm_upgrade), path("json/realm/<realm_uuid>/billing/upgrade", remote_realm_upgrade),
path("json/server/<server_uuid>/billing/upgrade", remote_server_upgrade), path("json/server/<server_uuid>/billing/upgrade", remote_server_upgrade),
path("json/realm/<realm_uuid>/billing/plan", update_plan_for_remote_realm),
path("json/server/<server_uuid>/billing/plan", update_plan_for_remote_server),
] ]
urlpatterns += [ urlpatterns += [

View File

@@ -26,6 +26,16 @@ from zilencer.models import RemoteRealm, RemoteZulipServer
billing_logger = logging.getLogger("corporate.stripe") billing_logger = logging.getLogger("corporate.stripe")
ALLOWED_PLANS_API_STATUS_VALUES = [
CustomerPlan.ACTIVE,
CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE,
CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE,
CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE,
CustomerPlan.FREE_TRIAL,
CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL,
CustomerPlan.ENDED,
]
@zulip_login_required @zulip_login_required
@typed_endpoint @typed_endpoint
@@ -46,6 +56,7 @@ def billing_page(
"admin_access": user.has_billing_access, "admin_access": user.has_billing_access,
"has_active_plan": False, "has_active_plan": False,
"org_name": user.realm.name, "org_name": user.realm.name,
"billing_base_url": "",
} }
if not user.has_billing_access: if not user.has_billing_access:
@@ -92,6 +103,7 @@ def remote_realm_billing_page(
"admin_access": billing_session.has_billing_access(), "admin_access": billing_session.has_billing_access(),
"has_active_plan": False, "has_active_plan": False,
"org_name": billing_session.remote_realm.name, "org_name": billing_session.remote_realm.name,
"billing_base_url": f"/realm/{billing_session.remote_realm.uuid}",
} }
if billing_session.remote_realm.plan_type == RemoteRealm.PLAN_TYPE_COMMUNITY: if billing_session.remote_realm.plan_type == RemoteRealm.PLAN_TYPE_COMMUNITY:
@@ -135,6 +147,7 @@ def remote_server_billing_page(
"admin_access": billing_session.has_billing_access(), "admin_access": billing_session.has_billing_access(),
"has_active_plan": False, "has_active_plan": False,
"org_name": billing_session.remote_server.hostname, "org_name": billing_session.remote_server.hostname,
"billing_base_url": f"/server/{billing_session.remote_server.uuid}",
} }
if billing_session.remote_server.plan_type == RemoteZulipServer.PLAN_TYPE_COMMUNITY: if billing_session.remote_server.plan_type == RemoteZulipServer.PLAN_TYPE_COMMUNITY:
@@ -185,17 +198,7 @@ def update_plan(
user: UserProfile, user: UserProfile,
status: Optional[int] = REQ( status: Optional[int] = REQ(
"status", "status",
json_validator=check_int_in( json_validator=check_int_in(ALLOWED_PLANS_API_STATUS_VALUES),
[
CustomerPlan.ACTIVE,
CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE,
CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE,
CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE,
CustomerPlan.FREE_TRIAL,
CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL,
CustomerPlan.ENDED,
]
),
default=None, default=None,
), ),
licenses: Optional[int] = REQ("licenses", json_validator=check_int, default=None), licenses: Optional[int] = REQ("licenses", json_validator=check_int, default=None),
@@ -213,3 +216,55 @@ def update_plan(
billing_session = RealmBillingSession(user=user) billing_session = RealmBillingSession(user=user)
billing_session.do_update_plan(update_plan_request) billing_session.do_update_plan(update_plan_request)
return json_success(request) return json_success(request)
@authenticated_remote_realm_management_endpoint
@has_request_variables
def update_plan_for_remote_realm(
request: HttpRequest,
billing_session: RemoteRealmBillingSession,
status: Optional[int] = REQ(
"status",
json_validator=check_int_in(ALLOWED_PLANS_API_STATUS_VALUES),
default=None,
),
licenses: Optional[int] = REQ("licenses", json_validator=check_int, default=None),
licenses_at_next_renewal: Optional[int] = REQ(
"licenses_at_next_renewal", json_validator=check_int, default=None
),
schedule: Optional[int] = REQ("schedule", json_validator=check_int, default=None),
) -> HttpResponse: # nocoverage
update_plan_request = UpdatePlanRequest(
status=status,
licenses=licenses,
licenses_at_next_renewal=licenses_at_next_renewal,
schedule=schedule,
)
billing_session.do_update_plan(update_plan_request)
return json_success(request)
@authenticated_remote_server_management_endpoint
@has_request_variables
def update_plan_for_remote_server(
request: HttpRequest,
billing_session: RemoteServerBillingSession,
status: Optional[int] = REQ(
"status",
json_validator=check_int_in(ALLOWED_PLANS_API_STATUS_VALUES),
default=None,
),
licenses: Optional[int] = REQ("licenses", json_validator=check_int, default=None),
licenses_at_next_renewal: Optional[int] = REQ(
"licenses_at_next_renewal", json_validator=check_int, default=None
),
schedule: Optional[int] = REQ("schedule", json_validator=check_int, default=None),
) -> HttpResponse: # nocoverage
update_plan_request = UpdatePlanRequest(
status=status,
licenses=licenses,
licenses_at_next_renewal=licenses_at_next_renewal,
schedule=schedule,
)
billing_session.do_update_plan(update_plan_request)
return json_success(request)

View File

@@ -12,7 +12,6 @@ from corporate.lib.stripe import (
RemoteRealmBillingSession, RemoteRealmBillingSession,
RemoteServerBillingSession, RemoteServerBillingSession,
) )
from corporate.models import Session
from zerver.decorator import require_billing_access, require_organization_member from zerver.decorator import require_billing_access, require_organization_member
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import typed_endpoint from zerver.lib.typed_endpoint import typed_endpoint
@@ -24,20 +23,32 @@ billing_logger = logging.getLogger("corporate.stripe")
@require_billing_access @require_billing_access
def start_card_update_stripe_session(request: HttpRequest, user: UserProfile) -> HttpResponse: def start_card_update_stripe_session(request: HttpRequest, user: UserProfile) -> HttpResponse:
billing_session = RealmBillingSession(user) billing_session = RealmBillingSession(user)
assert billing_session.get_customer() is not None session_data = billing_session.create_card_update_session()
metadata = {
"type": "card_update",
"user_id": user.id,
}
stripe_session = billing_session.create_stripe_checkout_session(
metadata, Session.CARD_UPDATE_FROM_BILLING_PAGE
)
return json_success( return json_success(
request, request,
data={ data=session_data,
"stripe_session_url": stripe_session.url, )
"stripe_session_id": stripe_session.id,
},
@authenticated_remote_realm_management_endpoint
def start_card_update_stripe_session_for_remote_realm(
request: HttpRequest, billing_session: RemoteRealmBillingSession
) -> HttpResponse: # nocoverage
session_data = billing_session.create_card_update_session()
return json_success(
request,
data=session_data,
)
@authenticated_remote_server_management_endpoint
def start_card_update_stripe_session_for_remote_server(
request: HttpRequest, billing_session: RemoteServerBillingSession
) -> HttpResponse: # nocoverage
session_data = billing_session.create_card_update_session()
return json_success(
request,
data=session_data,
) )
@@ -50,9 +61,7 @@ def start_card_update_stripe_session_for_realm_upgrade(
manual_license_management: Json[bool] = False, manual_license_management: Json[bool] = False,
) -> HttpResponse: ) -> HttpResponse:
billing_session = RealmBillingSession(user) billing_session = RealmBillingSession(user)
session_data = billing_session.get_card_update_session_data_for_upgrade( session_data = billing_session.create_card_update_session_for_upgrade(manual_license_management)
manual_license_management
)
return json_success( return json_success(
request, request,
data=session_data, data=session_data,
@@ -67,9 +76,7 @@ def start_card_update_stripe_session_for_remote_realm_upgrade(
*, *,
manual_license_management: Json[bool] = False, manual_license_management: Json[bool] = False,
) -> HttpResponse: # nocoverage ) -> HttpResponse: # nocoverage
session_data = billing_session.get_card_update_session_data_for_upgrade( session_data = billing_session.create_card_update_session_for_upgrade(manual_license_management)
manual_license_management
)
return json_success( return json_success(
request, request,
data=session_data, data=session_data,
@@ -84,9 +91,7 @@ def start_card_update_stripe_session_for_remote_server_upgrade(
*, *,
manual_license_management: Json[bool] = False, manual_license_management: Json[bool] = False,
) -> HttpResponse: # nocoverage ) -> HttpResponse: # nocoverage
session_data = billing_session.get_card_update_session_data_for_upgrade( session_data = billing_session.create_card_update_session_for_upgrade(manual_license_management)
manual_license_management
)
return json_success( return json_success(
request, request,
data=session_data, data=session_data,

View File

@@ -6,12 +6,12 @@
{% endblock %} {% endblock %}
{% block portico_content %} {% block portico_content %}
<div id="billing-page" class="register-account flex full-page"> <div id="billing-page" class="register-account flex full-page" data-billing-base-url="{{ billing_base_url }}">
<div class="center-block new-style"> <div class="center-block new-style">
{% if admin_access and has_active_plan %} {% if admin_access and has_active_plan %}
{% if is_sponsorship_pending %} {% if is_sponsorship_pending %}
<div class="alert alert-success billing-page-success" id="billing-sponsorship-pending-message-top"> <div class="alert alert-success billing-page-success" id="billing-sponsorship-pending-message-top">
This organization has requested sponsorship for a <a href="/plans/">{{ plan_name }}</a> plan. <a href="mailto:support@zulip.com">Contact Zulip support</a> with any questions or updates. This organization has requested sponsorship for a <a href="{{ billing_base_url }}/plans/">{{ plan_name }}</a> plan. <a href="mailto:support@zulip.com">Contact Zulip support</a> with any questions or updates.
</div> </div>
{% endif %} {% endif %}
{% if success_message %} {% if success_message %}
@@ -157,11 +157,11 @@
{% if downgrade_at_end_of_cycle %} {% if downgrade_at_end_of_cycle %}
Your organization will be downgraded to <strong>Zulip Cloud Free</strong> at the end of the current billing Your organization will be downgraded to <strong>Zulip Cloud Free</strong> at the end of the current billing
period (<strong>{{ renewal_date }}</strong>). You will lose access to unlimited search history and period (<strong>{{ renewal_date }}</strong>). You will lose access to unlimited search history and
<a href="/plans/">other features</a> of your current plan. <a href="{{ billing_base_url }}/plans/">other features</a> of your current plan.
{% elif downgrade_at_end_of_free_trial %} {% elif downgrade_at_end_of_free_trial %}
Your organization will be downgraded to <strong>Zulip Cloud Free</strong> at the end of the free trial Your organization will be downgraded to <strong>Zulip Cloud Free</strong> at the end of the free trial
(<strong>{{ renewal_date }}</strong>). You will lose access to unlimited search history and (<strong>{{ renewal_date }}</strong>). You will lose access to unlimited search history and
<a href="/plans/">other features</a> of your current plan. <a href="{{ billing_base_url }}/plans/">other features</a> of your current plan.
{% else %} {% else %}
{% if charge_automatically %} {% if charge_automatically %}
Your plan will automatically renew on <strong>{{ renewal_date }}</strong>. Your plan will automatically renew on <strong>{{ renewal_date }}</strong>.
@@ -276,7 +276,7 @@
<p> <p>
Your organization will be downgraded to <strong>Zulip Cloud Free</strong> at the end of your free trial Your organization will be downgraded to <strong>Zulip Cloud Free</strong> at the end of your free trial
({{ renewal_date }}). You will lose access to unlimited search history and ({{ renewal_date }}). You will lose access to unlimited search history and
<a href="/plans/">other features</a> <a href="{{ billing_base_url }}/plans/">other features</a>
of your current plan. Are you sure you want to continue? of your current plan. Are you sure you want to continue?
</p> </p>
</main> </main>
@@ -360,7 +360,7 @@
<p> <p>
Your organization will be downgraded to <strong>Zulip Cloud Free</strong> at the end of the current billing Your organization will be downgraded to <strong>Zulip Cloud Free</strong> at the end of the current billing
period ({{ renewal_date }}). You will lose access to unlimited search history and period ({{ renewal_date }}). You will lose access to unlimited search history and
<a href="/plans/">other features</a> <a href="{{ billing_base_url }}/plans/">other features</a>
of your current plan. Are you sure you want to continue? of your current plan. Are you sure you want to continue?
</p> </p>
</main> </main>

View File

@@ -6,6 +6,7 @@ import * as portico_modals from "../portico/portico_modals";
import * as helpers from "./helpers"; import * as helpers from "./helpers";
const billing_frequency_schema = z.enum(["Monthly", "Annual"]); const billing_frequency_schema = z.enum(["Monthly", "Annual"]);
const billing_base_url = $("#billing-page").attr("data-billing-base-url")!;
// Matches the CustomerPlan model in the backend. // Matches the CustomerPlan model in the backend.
enum BillingFrequency { enum BillingFrequency {
@@ -25,13 +26,13 @@ export function create_update_current_cycle_license_request(): void {
$("#current-manual-license-count-update-button .billing-button-text").text(""); $("#current-manual-license-count-update-button .billing-button-text").text("");
$("#current-manual-license-count-update-button .loader").show(); $("#current-manual-license-count-update-button .loader").show();
helpers.create_ajax_request( helpers.create_ajax_request(
"/json/billing/plan", `/json${billing_base_url}/billing/plan`,
"current-license-change", "current-license-change",
[], [],
"PATCH", "post",
() => { () => {
window.location.replace( window.location.replace(
"/billing/?success_message=" + `${billing_base_url}/billing/?success_message=` +
encodeURIComponent( encodeURIComponent(
"Updated number of licenses for the current billing period.", "Updated number of licenses for the current billing period.",
), ),
@@ -50,13 +51,13 @@ export function create_update_next_cycle_license_request(): void {
$("#next-manual-license-count-update-button .loader").show(); $("#next-manual-license-count-update-button .loader").show();
$("#next-manual-license-count-update-button .billing-button-text").text(""); $("#next-manual-license-count-update-button .billing-button-text").text("");
helpers.create_ajax_request( helpers.create_ajax_request(
"/json/billing/plan", `/json${billing_base_url}/billing/plan`,
"next-license-change", "next-license-change",
[], [],
"PATCH", "post",
() => { () => {
window.location.replace( window.location.replace(
"/billing/?success_message=" + `${billing_base_url}/billing/?success_message=` +
encodeURIComponent("Updated number of licenses for the next billing period."), encodeURIComponent("Updated number of licenses for the next billing period."),
); );
$("#next-manual-license-count-update-button .loader").hide(); $("#next-manual-license-count-update-button .loader").hide();
@@ -74,7 +75,7 @@ export function initialize(): void {
$("#update-card-button .billing-button-text").text(""); $("#update-card-button .billing-button-text").text("");
$("#update-card-button .loader").show(); $("#update-card-button .loader").show();
helpers.create_ajax_request( helpers.create_ajax_request(
"/json/billing/session/start_card_update_session", `/json${billing_base_url}/billing/session/start_card_update_session`,
"cardchange", "cardchange",
[], [],
"POST", "POST",
@@ -188,9 +189,14 @@ export function initialize(): void {
); );
$("#confirm-cancel-subscription-modal .dialog_submit_button").on("click", (e) => { $("#confirm-cancel-subscription-modal .dialog_submit_button").on("click", (e) => {
helpers.create_ajax_request("/json/billing/plan", "planchange", [], "PATCH", () => helpers.create_ajax_request(
`/json${billing_base_url}/billing/plan`,
"planchange",
[],
"post",
() =>
window.location.replace( window.location.replace(
"/billing/?success_message=" + `${billing_base_url}/billing/?success_message=` +
encodeURIComponent("Your plan has been canceled and will not renew."), encodeURIComponent("Your plan has been canceled and will not renew."),
), ),
); );
@@ -198,9 +204,14 @@ export function initialize(): void {
}); });
$("#reactivate-subscription .reactivate-current-plan-button").on("click", (e) => { $("#reactivate-subscription .reactivate-current-plan-button").on("click", (e) => {
helpers.create_ajax_request("/json/billing/plan", "planchange", [], "PATCH", () => helpers.create_ajax_request(
`/json${billing_base_url}/billing/plan`,
"planchange",
[],
"post",
() =>
window.location.replace( window.location.replace(
"/billing/?success_message=" + `${billing_base_url}/billing/?success_message=` +
encodeURIComponent( encodeURIComponent(
"Your plan has been reactivated and will renew automatically.", "Your plan has been reactivated and will renew automatically.",
), ),
@@ -210,9 +221,14 @@ export function initialize(): void {
}); });
$("#confirm-end-free-trial .dialog_submit_button").on("click", (e) => { $("#confirm-end-free-trial .dialog_submit_button").on("click", (e) => {
helpers.create_ajax_request("/json/billing/plan", "planchange", [], "PATCH", () => helpers.create_ajax_request(
`/json${billing_base_url}/billing/plan`,
"planchange",
[],
"post",
() =>
window.location.replace( window.location.replace(
"/billing/?success_message=" + `${billing_base_url}/billing/?success_message=` +
encodeURIComponent( encodeURIComponent(
"Your plan will be canceled at the end of the trial. Your card will not be charged.", "Your plan will be canceled at the end of the trial. Your card will not be charged.",
), ),
@@ -326,12 +342,12 @@ export function initialize(): void {
}; };
e.preventDefault(); e.preventDefault();
void $.ajax({ void $.ajax({
type: "patch", type: "post",
url: "/json/billing/plan", url: `/json${billing_base_url}/billing/plan`,
data, data,
success() { success() {
window.location.replace( window.location.replace(
"/billing/?success_message=" + `${billing_base_url}/billing/?success_message=` +
encodeURIComponent("Billing frequency has been updated."), encodeURIComponent("Billing frequency has been updated."),
); );
}, },

View File

@@ -42,7 +42,7 @@ function handle_session_complete_event(session: StripeSession): void {
let redirect_to = ""; let redirect_to = "";
switch (session.type) { switch (session.type) {
case "card_update_from_billing_page": case "card_update_from_billing_page":
redirect_to = "/billing/"; redirect_to = billing_base_url + "/billing/";
break; break;
case "card_update_from_upgrade_page": case "card_update_from_upgrade_page":
redirect_to = helpers.get_upgrade_page_url( redirect_to = helpers.get_upgrade_page_url(