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
def get_card_update_session_data_for_upgrade(
def create_card_update_session_for_upgrade(
self,
manual_license_management: bool,
) -> Dict[str, Any]:
@@ -892,12 +892,8 @@ class BillingSession(ABC):
"stripe_session_id": stripe_session.id,
}
def create_stripe_checkout_session(
self,
metadata: Dict[str, Any],
session_type: int,
) -> stripe.checkout.Session:
# Create checkout sessions for adding and updating exiting cards.
def create_card_update_session(self) -> Dict[str, Any]:
metadata = self.get_metadata_for_stripe_update_card()
customer = self.get_customer()
assert customer is not None and customer.stripe_customer_id is not None
stripe_session = stripe.checkout.Session.create(
@@ -911,9 +907,12 @@ class BillingSession(ABC):
Session.objects.create(
stripe_session_id=stripe_session.id,
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:
customer = self.get_customer()

View File

@@ -9,6 +9,8 @@ from corporate.views.billing_page import (
remote_realm_billing_page,
remote_server_billing_page,
update_plan,
update_plan_for_remote_realm,
update_plan_for_remote_server,
)
from corporate.views.event_status import (
event_status,
@@ -36,7 +38,9 @@ from corporate.views.remote_billing_page import (
from corporate.views.session import (
start_card_update_stripe_session,
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_server,
start_card_update_stripe_session_for_remote_server_upgrade,
)
from corporate.views.sponsorship import (
@@ -231,6 +235,14 @@ urlpatterns += [
# Remote variants of above API endpoints.
path("json/realm/<realm_uuid>/sponsorship", remote_realm_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(
"json/realm/<realm_uuid>/upgrade/session/start_card_update_session",
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/realm/<realm_uuid>/billing/upgrade", remote_realm_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 += [

View File

@@ -26,6 +26,16 @@ from zilencer.models import RemoteRealm, RemoteZulipServer
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
@typed_endpoint
@@ -46,6 +56,7 @@ def billing_page(
"admin_access": user.has_billing_access,
"has_active_plan": False,
"org_name": user.realm.name,
"billing_base_url": "",
}
if not user.has_billing_access:
@@ -92,6 +103,7 @@ def remote_realm_billing_page(
"admin_access": billing_session.has_billing_access(),
"has_active_plan": False,
"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:
@@ -135,6 +147,7 @@ def remote_server_billing_page(
"admin_access": billing_session.has_billing_access(),
"has_active_plan": False,
"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:
@@ -185,17 +198,7 @@ def update_plan(
user: UserProfile,
status: Optional[int] = REQ(
"status",
json_validator=check_int_in(
[
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,
]
),
json_validator=check_int_in(ALLOWED_PLANS_API_STATUS_VALUES),
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.do_update_plan(update_plan_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,
RemoteServerBillingSession,
)
from corporate.models import Session
from zerver.decorator import require_billing_access, require_organization_member
from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import typed_endpoint
@@ -24,20 +23,32 @@ billing_logger = logging.getLogger("corporate.stripe")
@require_billing_access
def start_card_update_stripe_session(request: HttpRequest, user: UserProfile) -> HttpResponse:
billing_session = RealmBillingSession(user)
assert billing_session.get_customer() is not None
metadata = {
"type": "card_update",
"user_id": user.id,
}
stripe_session = billing_session.create_stripe_checkout_session(
metadata, Session.CARD_UPDATE_FROM_BILLING_PAGE
)
session_data = billing_session.create_card_update_session()
return json_success(
request,
data={
"stripe_session_url": stripe_session.url,
"stripe_session_id": stripe_session.id,
},
data=session_data,
)
@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,
) -> HttpResponse:
billing_session = RealmBillingSession(user)
session_data = billing_session.get_card_update_session_data_for_upgrade(
manual_license_management
)
session_data = billing_session.create_card_update_session_for_upgrade(manual_license_management)
return json_success(
request,
data=session_data,
@@ -67,9 +76,7 @@ def start_card_update_stripe_session_for_remote_realm_upgrade(
*,
manual_license_management: Json[bool] = False,
) -> HttpResponse: # nocoverage
session_data = billing_session.get_card_update_session_data_for_upgrade(
manual_license_management
)
session_data = billing_session.create_card_update_session_for_upgrade(manual_license_management)
return json_success(
request,
data=session_data,
@@ -84,9 +91,7 @@ def start_card_update_stripe_session_for_remote_server_upgrade(
*,
manual_license_management: Json[bool] = False,
) -> HttpResponse: # nocoverage
session_data = billing_session.get_card_update_session_data_for_upgrade(
manual_license_management
)
session_data = billing_session.create_card_update_session_for_upgrade(manual_license_management)
return json_success(
request,
data=session_data,

View File

@@ -6,12 +6,12 @@
{% endblock %}
{% 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">
{% if admin_access and has_active_plan %}
{% if is_sponsorship_pending %}
<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>
{% endif %}
{% if success_message %}
@@ -157,11 +157,11 @@
{% if downgrade_at_end_of_cycle %}
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
<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 %}
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
<a href="/plans/">other features</a> of your current plan.
<a href="{{ billing_base_url }}/plans/">other features</a> of your current plan.
{% else %}
{% if charge_automatically %}
Your plan will automatically renew on <strong>{{ renewal_date }}</strong>.
@@ -276,7 +276,7 @@
<p>
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
<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?
</p>
</main>
@@ -360,7 +360,7 @@
<p>
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
<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?
</p>
</main>

View File

@@ -6,6 +6,7 @@ import * as portico_modals from "../portico/portico_modals";
import * as helpers from "./helpers";
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.
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 .loader").show();
helpers.create_ajax_request(
"/json/billing/plan",
`/json${billing_base_url}/billing/plan`,
"current-license-change",
[],
"PATCH",
"post",
() => {
window.location.replace(
"/billing/?success_message=" +
`${billing_base_url}/billing/?success_message=` +
encodeURIComponent(
"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 .billing-button-text").text("");
helpers.create_ajax_request(
"/json/billing/plan",
`/json${billing_base_url}/billing/plan`,
"next-license-change",
[],
"PATCH",
"post",
() => {
window.location.replace(
"/billing/?success_message=" +
`${billing_base_url}/billing/?success_message=` +
encodeURIComponent("Updated number of licenses for the next billing period."),
);
$("#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 .loader").show();
helpers.create_ajax_request(
"/json/billing/session/start_card_update_session",
`/json${billing_base_url}/billing/session/start_card_update_session`,
"cardchange",
[],
"POST",
@@ -188,9 +189,14 @@ export function initialize(): void {
);
$("#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(
"/billing/?success_message=" +
`${billing_base_url}/billing/?success_message=` +
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) => {
helpers.create_ajax_request("/json/billing/plan", "planchange", [], "PATCH", () =>
helpers.create_ajax_request(
`/json${billing_base_url}/billing/plan`,
"planchange",
[],
"post",
() =>
window.location.replace(
"/billing/?success_message=" +
`${billing_base_url}/billing/?success_message=` +
encodeURIComponent(
"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) => {
helpers.create_ajax_request("/json/billing/plan", "planchange", [], "PATCH", () =>
helpers.create_ajax_request(
`/json${billing_base_url}/billing/plan`,
"planchange",
[],
"post",
() =>
window.location.replace(
"/billing/?success_message=" +
`${billing_base_url}/billing/?success_message=` +
encodeURIComponent(
"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();
void $.ajax({
type: "patch",
url: "/json/billing/plan",
type: "post",
url: `/json${billing_base_url}/billing/plan`,
data,
success() {
window.location.replace(
"/billing/?success_message=" +
`${billing_base_url}/billing/?success_message=` +
encodeURIComponent("Billing frequency has been updated."),
);
},

View File

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