billing: Allow free trial orgs to switch billing frequency.

Fixes #27855
This commit is contained in:
Aman Agrawal
2023-11-26 14:41:28 +00:00
committed by Tim Abbott
parent 1f8d3fc48f
commit b35a792623
31 changed files with 1630 additions and 9 deletions

View File

@@ -491,6 +491,7 @@ class UpdatePlanRequest:
status: Optional[int] status: Optional[int]
licenses: Optional[int] licenses: Optional[int]
licenses_at_next_renewal: Optional[int] licenses_at_next_renewal: Optional[int]
schedule: Optional[int]
@dataclass @dataclass
@@ -1035,6 +1036,72 @@ class BillingSession(ABC):
data["stripe_payment_intent_id"] = stripe_payment_intent_id data["stripe_payment_intent_id"] = stripe_payment_intent_id
return data return data
def do_change_schedule_after_free_trial(self, plan: CustomerPlan, schedule: int) -> None:
# Change the billing frequency of the plan after the free trial ends.
assert schedule in (CustomerPlan.MONTHLY, CustomerPlan.ANNUAL)
last_ledger_entry = LicenseLedger.objects.filter(plan=plan).order_by("-id").first()
assert last_ledger_entry is not None
licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal
assert licenses_at_next_renewal is not None
assert plan.next_invoice_date is not None
next_billing_cycle = plan.next_invoice_date
if plan.fixed_price is not None: # nocoverage
raise BillingError("Customer is already on monthly fixed plan.")
plan.status = CustomerPlan.ENDED
plan.save(update_fields=["status"])
discount = plan.customer.default_discount or plan.discount
_, _, _, price_per_license = compute_plan_parameters(
tier=plan.tier,
automanage_licenses=plan.automanage_licenses,
billing_schedule=schedule,
discount=plan.discount,
)
new_plan = CustomerPlan.objects.create(
customer=plan.customer,
billing_schedule=schedule,
automanage_licenses=plan.automanage_licenses,
charge_automatically=plan.charge_automatically,
price_per_license=price_per_license,
discount=discount,
billing_cycle_anchor=plan.billing_cycle_anchor,
tier=plan.tier,
status=CustomerPlan.FREE_TRIAL,
next_invoice_date=next_billing_cycle,
invoiced_through=None,
invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT,
)
LicenseLedger.objects.create(
plan=new_plan,
is_renewal=True,
event_time=plan.billing_cycle_anchor,
licenses=licenses_at_next_renewal,
licenses_at_next_renewal=licenses_at_next_renewal,
)
if schedule == CustomerPlan.ANNUAL:
self.write_to_audit_log(
event_type=AuditLogEventType.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN,
event_time=timezone_now(),
extra_data={
"monthly_plan_id": plan.id,
"annual_plan_id": new_plan.id,
},
)
else:
self.write_to_audit_log(
event_type=AuditLogEventType.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN,
event_time=timezone_now(),
extra_data={
"annual_plan_id": plan.id,
"monthly_plan_id": new_plan.id,
},
)
def get_next_billing_cycle(self, plan: CustomerPlan) -> datetime: def get_next_billing_cycle(self, plan: CustomerPlan) -> datetime:
last_ledger_renewal = ( last_ledger_renewal = (
LicenseLedger.objects.filter(plan=plan, is_renewal=True).order_by("-id").first() LicenseLedger.objects.filter(plan=plan, is_renewal=True).order_by("-id").first()
@@ -1061,8 +1128,9 @@ class BillingSession(ABC):
) -> Tuple[Optional[CustomerPlan], Optional[LicenseLedger]]: ) -> Tuple[Optional[CustomerPlan], Optional[LicenseLedger]]:
last_ledger_entry = LicenseLedger.objects.filter(plan=plan).order_by("-id").first() last_ledger_entry = LicenseLedger.objects.filter(plan=plan).order_by("-id").first()
next_billing_cycle = self.get_next_billing_cycle(plan) next_billing_cycle = self.get_next_billing_cycle(plan)
event_in_next_billing_cycle = next_billing_cycle <= event_time
if next_billing_cycle <= event_time and last_ledger_entry is not None: if event_in_next_billing_cycle and last_ledger_entry is not None:
licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal
assert licenses_at_next_renewal is not None assert licenses_at_next_renewal is not None
if plan.status == CustomerPlan.ACTIVE: if plan.status == CustomerPlan.ACTIVE:
@@ -1187,6 +1255,7 @@ class BillingSession(ABC):
if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE: if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
self.process_downgrade(plan) self.process_downgrade(plan)
return None, None return None, None
return None, last_ledger_entry return None, last_ledger_entry
@@ -1441,6 +1510,9 @@ class BillingSession(ABC):
assert plan.is_free_trial() assert plan.is_free_trial()
do_change_plan_status(plan, status) do_change_plan_status(plan, status)
elif status == CustomerPlan.FREE_TRIAL: elif status == CustomerPlan.FREE_TRIAL:
if update_plan_request.schedule is not None:
self.do_change_schedule_after_free_trial(plan, update_plan_request.schedule)
else:
assert plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL assert plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL
do_change_plan_status(plan, status) do_change_plan_status(plan, status)
return return

View File

@@ -0,0 +1,33 @@
{
"address": null,
"balance": 0,
"created": 1701007131,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_P4scN21egyF63j",
"invoice_prefix": "BFEDDCEB",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "2",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,33 @@
{
"address": null,
"balance": 0,
"created": 1701007131,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_P4scN21egyF63j",
"invoice_prefix": "BFEDDCEB",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": "pm_1OGirJDEQaroqDjs0fg5PtPl",
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "2",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,79 @@
{
"address": null,
"balance": 0,
"created": 1701007131,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_P4scN21egyF63j",
"invoice_prefix": "BFEDDCEB",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": {
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 11,
"exp_year": 2024,
"fingerprint": "9XKsMixKBi6kIIzd",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 1701007133,
"customer": "cus_P4scN21egyF63j",
"id": "pm_1OGirJDEQaroqDjs0fg5PtPl",
"livemode": false,
"metadata": {},
"object": "payment_method",
"type": "card"
},
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "2",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,79 @@
{
"address": null,
"balance": 0,
"created": 1701007131,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_P4scN21egyF63j",
"invoice_prefix": "BFEDDCEB",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": {
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 11,
"exp_year": 2024,
"fingerprint": "9XKsMixKBi6kIIzd",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 1701007133,
"customer": "cus_P4scN21egyF63j",
"id": "pm_1OGirJDEQaroqDjs0fg5PtPl",
"livemode": false,
"metadata": {},
"object": "payment_method",
"type": "card"
},
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "2",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,79 @@
{
"address": null,
"balance": 0,
"created": 1701007131,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_P4scN21egyF63j",
"invoice_prefix": "BFEDDCEB",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": {
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 11,
"exp_year": 2024,
"fingerprint": "9XKsMixKBi6kIIzd",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 1701007133,
"customer": "cus_P4scN21egyF63j",
"id": "pm_1OGirJDEQaroqDjs0fg5PtPl",
"livemode": false,
"metadata": {},
"object": "payment_method",
"type": "card"
},
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "2",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,79 @@
{
"address": null,
"balance": 0,
"created": 1701007131,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_P4scN21egyF63j",
"invoice_prefix": "BFEDDCEB",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": {
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 11,
"exp_year": 2024,
"fingerprint": "9XKsMixKBi6kIIzd",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 1701007133,
"customer": "cus_P4scN21egyF63j",
"id": "pm_1OGirJDEQaroqDjs0fg5PtPl",
"livemode": false,
"metadata": {},
"object": "payment_method",
"type": "card"
},
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "2",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,60 @@
{
"data": [
{
"api_version": "2020-08-27",
"created": 1701007135,
"data": {
"object": {
"address": null,
"balance": 0,
"created": 1701007131,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_P4scN21egyF63j",
"invoice_prefix": "BFEDDCEB",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": "pm_1OGirJDEQaroqDjs0fg5PtPl",
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "2",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
},
"previous_attributes": {
"invoice_settings": {
"default_payment_method": null
}
}
},
"id": "evt_1OGirLDEQaroqDjsz8SKiaqo",
"livemode": false,
"object": "event",
"pending_webhooks": 0,
"request": {
"id": "req_AyeuVEyieC6onh",
"idempotency_key": "89b1c0ff-4445-484e-a46d-edb738e19727"
},
"type": "customer.updated"
}
],
"has_more": true,
"object": "list",
"url": "/v1/events"
}

View File

@@ -0,0 +1,6 @@
{
"data": [],
"has_more": false,
"object": "list",
"url": "/v1/invoices"
}

View File

@@ -0,0 +1,34 @@
{
"application": null,
"automatic_payment_methods": null,
"cancellation_reason": null,
"client_secret": "seti_1OGirJDEQaroqDjsYpipSEB0_secret_P4scaxG6yYSKO4CtptkXClW7kitH4Z5",
"created": 1701007133,
"customer": "cus_P4scN21egyF63j",
"description": null,
"flow_directions": null,
"id": "seti_1OGirJDEQaroqDjsYpipSEB0",
"last_setup_error": null,
"latest_attempt": "setatt_1OGirJDEQaroqDjskmjNC3F7",
"livemode": false,
"mandate": null,
"metadata": {},
"next_action": null,
"object": "setup_intent",
"on_behalf_of": null,
"payment_method": "pm_1OGirJDEQaroqDjs0fg5PtPl",
"payment_method_configuration_details": null,
"payment_method_options": {
"card": {
"mandate_options": null,
"network": null,
"request_three_d_secure": "automatic"
}
},
"payment_method_types": [
"card"
],
"single_use_mandate": null,
"status": "succeeded",
"usage": "off_session"
}

View File

@@ -0,0 +1,41 @@
{
"data": [
{
"application": null,
"automatic_payment_methods": null,
"cancellation_reason": null,
"client_secret": "seti_1OGirHDEQaroqDjsWRgeKxys_secret_P4scp8ZoQF4uJncgCdd4XzNWWTw16yl",
"created": 1701007131,
"customer": "cus_P4scN21egyF63j",
"description": null,
"flow_directions": null,
"id": "seti_1OGirHDEQaroqDjsWRgeKxys",
"last_setup_error": null,
"latest_attempt": null,
"livemode": false,
"mandate": null,
"metadata": {},
"next_action": null,
"object": "setup_intent",
"on_behalf_of": null,
"payment_method": null,
"payment_method_configuration_details": null,
"payment_method_options": {
"card": {
"mandate_options": null,
"network": null,
"request_three_d_secure": "automatic"
}
},
"payment_method_types": [
"card"
],
"single_use_mandate": null,
"status": "requires_payment_method",
"usage": "off_session"
}
],
"has_more": true,
"object": "list",
"url": "/v1/setup_intents"
}

View File

@@ -0,0 +1,34 @@
{
"application": null,
"automatic_payment_methods": null,
"cancellation_reason": null,
"client_secret": "seti_1OGirJDEQaroqDjsYpipSEB0_secret_P4scaxG6yYSKO4CtptkXClW7kitH4Z5",
"created": 1701007133,
"customer": "cus_P4scN21egyF63j",
"description": null,
"flow_directions": null,
"id": "seti_1OGirJDEQaroqDjsYpipSEB0",
"last_setup_error": null,
"latest_attempt": "setatt_1OGirJDEQaroqDjskmjNC3F7",
"livemode": false,
"mandate": null,
"metadata": {},
"next_action": null,
"object": "setup_intent",
"on_behalf_of": null,
"payment_method": "pm_1OGirJDEQaroqDjs0fg5PtPl",
"payment_method_configuration_details": null,
"payment_method_options": {
"card": {
"mandate_options": null,
"network": null,
"request_three_d_secure": "automatic"
}
},
"payment_method_types": [
"card"
],
"single_use_mandate": null,
"status": "succeeded",
"usage": "off_session"
}

View File

@@ -0,0 +1,73 @@
{
"after_expiration": null,
"allow_promotion_codes": null,
"amount_subtotal": null,
"amount_total": null,
"automatic_tax": {
"enabled": false,
"status": null
},
"billing_address_collection": null,
"cancel_url": "http://zulip.testserver/upgrade/",
"client_reference_id": null,
"client_secret": null,
"consent": null,
"consent_collection": null,
"created": 1701007131,
"currency": null,
"currency_conversion": null,
"custom_fields": [],
"custom_text": {
"shipping_address": null,
"submit": null,
"terms_of_service_acceptance": null
},
"customer": "cus_P4scN21egyF63j",
"customer_creation": null,
"customer_details": {
"address": null,
"email": "hamlet@zulip.com",
"name": null,
"phone": null,
"tax_exempt": null,
"tax_ids": null
},
"customer_email": null,
"expires_at": 1701093531,
"id": "cs_test_c1fBDZcNaDnv5cqg1WNBXNCDmlUwwLBP59mIYRwRB1OUAEYocADHCi5EOH",
"invoice": null,
"invoice_creation": null,
"livemode": false,
"locale": null,
"metadata": {
"type": "card_update",
"user_id": "10"
},
"mode": "setup",
"object": "checkout.session",
"payment_intent": null,
"payment_link": null,
"payment_method_collection": "always",
"payment_method_configuration_details": null,
"payment_method_options": {},
"payment_method_types": [
"card"
],
"payment_status": "no_payment_required",
"phone_number_collection": {
"enabled": false
},
"recovered_from": null,
"setup_intent": "seti_1OGirHDEQaroqDjsWRgeKxys",
"shipping": null,
"shipping_address_collection": null,
"shipping_options": [],
"shipping_rate": null,
"status": "open",
"submit_type": null,
"subscription": null,
"success_url": "http://zulip.testserver/billing/event_status?stripe_session_id={CHECKOUT_SESSION_ID}",
"total_details": null,
"ui_mode": "hosted",
"url": "https://checkout.stripe.com/c/pay/cs_test_c1fBDZcNaDnv5cqg1WNBXNCDmlUwwLBP59mIYRwRB1OUAEYocADHCi5EOH#fidkdWxOYHwnPyd1blpxYHZxWl1UZjFOczZJXUE2PUpzUWNPV2ZpdlFzUCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBaZmppcGhrJyknYGtkZ2lgVWlkZmBtamlhYHd2Jz9xd3BgeCUl"
}

View File

@@ -0,0 +1,80 @@
{
"data": [
{
"after_expiration": null,
"allow_promotion_codes": null,
"amount_subtotal": null,
"amount_total": null,
"automatic_tax": {
"enabled": false,
"status": null
},
"billing_address_collection": null,
"cancel_url": "http://zulip.testserver/upgrade/",
"client_reference_id": null,
"client_secret": null,
"consent": null,
"consent_collection": null,
"created": 1701007131,
"currency": null,
"currency_conversion": null,
"custom_fields": [],
"custom_text": {
"shipping_address": null,
"submit": null,
"terms_of_service_acceptance": null
},
"customer": "cus_P4scN21egyF63j",
"customer_creation": null,
"customer_details": {
"address": null,
"email": "hamlet@zulip.com",
"name": null,
"phone": null,
"tax_exempt": null,
"tax_ids": null
},
"customer_email": null,
"expires_at": 1701093531,
"id": "cs_test_c1fBDZcNaDnv5cqg1WNBXNCDmlUwwLBP59mIYRwRB1OUAEYocADHCi5EOH",
"invoice": null,
"invoice_creation": null,
"livemode": false,
"locale": null,
"metadata": {
"type": "card_update",
"user_id": "10"
},
"mode": "setup",
"object": "checkout.session",
"payment_intent": null,
"payment_link": null,
"payment_method_collection": "always",
"payment_method_configuration_details": null,
"payment_method_options": {},
"payment_method_types": [
"card"
],
"payment_status": "no_payment_required",
"phone_number_collection": {
"enabled": false
},
"recovered_from": null,
"setup_intent": "seti_1OGirHDEQaroqDjsWRgeKxys",
"shipping": null,
"shipping_address_collection": null,
"shipping_options": [],
"shipping_rate": null,
"status": "open",
"submit_type": null,
"subscription": null,
"success_url": "http://zulip.testserver/billing/event_status?stripe_session_id={CHECKOUT_SESSION_ID}",
"total_details": null,
"ui_mode": "hosted",
"url": "https://checkout.stripe.com/c/pay/cs_test_c1fBDZcNaDnv5cqg1WNBXNCDmlUwwLBP59mIYRwRB1OUAEYocADHCi5EOH#fidkdWxOYHwnPyd1blpxYHZxWl1UZjFOczZJXUE2PUpzUWNPV2ZpdlFzUCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBaZmppcGhrJyknYGtkZ2lgVWlkZmBtamlhYHd2Jz9xd3BgeCUl"
}
],
"has_more": true,
"object": "list",
"url": "/v1/checkout/sessions"
}

View File

@@ -0,0 +1,33 @@
{
"address": null,
"balance": 0,
"created": 1000000000,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001",
"invoice_prefix": "NORMA01",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "1",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,33 @@
{
"address": null,
"balance": 0,
"created": 1000000000,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001",
"invoice_prefix": "NORMA01",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": "pm_1OGjOiDEQaroqDjs1saPoOzz",
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "1",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,79 @@
{
"address": null,
"balance": 0,
"created": 1000000000,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001",
"invoice_prefix": "NORMA01",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": {
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 11,
"exp_year": 2024,
"fingerprint": "NORMALIZED000001",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 1000000000,
"customer": "cus_NORMALIZED0001",
"id": "pm_1OGjOiDEQaroqDjs1saPoOzz",
"livemode": false,
"metadata": {},
"object": "payment_method",
"type": "card"
},
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "1",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,79 @@
{
"address": null,
"balance": 0,
"created": 1000000000,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001",
"invoice_prefix": "NORMA01",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": {
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 11,
"exp_year": 2024,
"fingerprint": "NORMALIZED000001",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 1000000000,
"customer": "cus_NORMALIZED0001",
"id": "pm_1OGjOiDEQaroqDjs1saPoOzz",
"livemode": false,
"metadata": {},
"object": "payment_method",
"type": "card"
},
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "1",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,79 @@
{
"address": null,
"balance": 0,
"created": 1000000000,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001",
"invoice_prefix": "NORMA01",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": {
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 11,
"exp_year": 2024,
"fingerprint": "NORMALIZED000001",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 1000000000,
"customer": "cus_NORMALIZED0001",
"id": "pm_1OGjOiDEQaroqDjs1saPoOzz",
"livemode": false,
"metadata": {},
"object": "payment_method",
"type": "card"
},
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "1",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,79 @@
{
"address": null,
"balance": 0,
"created": 1000000000,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001",
"invoice_prefix": "NORMA01",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": {
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 11,
"exp_year": 2024,
"fingerprint": "NORMALIZED000001",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 1000000000,
"customer": "cus_NORMALIZED0001",
"id": "pm_1OGjOiDEQaroqDjs1saPoOzz",
"livemode": false,
"metadata": {},
"object": "payment_method",
"type": "card"
},
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "1",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}

View File

@@ -0,0 +1,60 @@
{
"data": [
{
"api_version": "2020-08-27",
"created": 1000000000,
"data": {
"object": {
"address": null,
"balance": 0,
"created": 1000000000,
"currency": null,
"default_currency": null,
"default_source": null,
"delinquent": false,
"description": "zulip (Zulip Dev)",
"discount": null,
"email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001",
"invoice_prefix": "NORMA01",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": "pm_1OGjOiDEQaroqDjs1saPoOzz",
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"realm_id": "1",
"realm_str": "zulip"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
},
"previous_attributes": {
"invoice_settings": {
"default_payment_method": null
}
}
},
"id": "evt_1OGjOjDEQaroqDjsgh56akwA",
"livemode": false,
"object": "event",
"pending_webhooks": 0,
"request": {
"id": "req_NORMALIZED0001",
"idempotency_key": "82ec0759-4649-4aa5-ab13-f80c5b6300cc"
},
"type": "customer.updated"
}
],
"has_more": true,
"object": "list",
"url": "/v1/events"
}

View File

@@ -0,0 +1,6 @@
{
"data": [],
"has_more": false,
"object": "list",
"url": "/v1/invoices"
}

View File

@@ -0,0 +1,34 @@
{
"application": null,
"automatic_payment_methods": null,
"cancellation_reason": null,
"client_secret": "seti_1OGjOiDEQaroqDjs0mczPK5Q_secret_P4tBceetMSlLfbfrbp4RXKdBjnNp4In",
"created": 1000000000,
"customer": "cus_NORMALIZED0001",
"description": null,
"flow_directions": null,
"id": "seti_1OGjOiDEQaroqDjs0mczPK5Q",
"last_setup_error": null,
"latest_attempt": "setatt_1OGjOiDEQaroqDjsZve6lwXi",
"livemode": false,
"mandate": null,
"metadata": {},
"next_action": null,
"object": "setup_intent",
"on_behalf_of": null,
"payment_method": "pm_1OGjOiDEQaroqDjs1saPoOzz",
"payment_method_configuration_details": null,
"payment_method_options": {
"card": {
"mandate_options": null,
"network": null,
"request_three_d_secure": "automatic"
}
},
"payment_method_types": [
"card"
],
"single_use_mandate": null,
"status": "succeeded",
"usage": "off_session"
}

View File

@@ -0,0 +1,41 @@
{
"data": [
{
"application": null,
"automatic_payment_methods": null,
"cancellation_reason": null,
"client_secret": "seti_1OGjOgDEQaroqDjsnqXN4MEe_secret_P4tBu0OljOeHnxDc1xFcHJLxe3YbMFs",
"created": 1000000000,
"customer": "cus_NORMALIZED0001",
"description": null,
"flow_directions": null,
"id": "seti_1OGjOgDEQaroqDjsnqXN4MEe",
"last_setup_error": null,
"latest_attempt": null,
"livemode": false,
"mandate": null,
"metadata": {},
"next_action": null,
"object": "setup_intent",
"on_behalf_of": null,
"payment_method": null,
"payment_method_configuration_details": null,
"payment_method_options": {
"card": {
"mandate_options": null,
"network": null,
"request_three_d_secure": "automatic"
}
},
"payment_method_types": [
"card"
],
"single_use_mandate": null,
"status": "requires_payment_method",
"usage": "off_session"
}
],
"has_more": true,
"object": "list",
"url": "/v1/setup_intents"
}

View File

@@ -0,0 +1,34 @@
{
"application": null,
"automatic_payment_methods": null,
"cancellation_reason": null,
"client_secret": "seti_1OGjOiDEQaroqDjs0mczPK5Q_secret_P4tBceetMSlLfbfrbp4RXKdBjnNp4In",
"created": 1000000000,
"customer": "cus_NORMALIZED0001",
"description": null,
"flow_directions": null,
"id": "seti_1OGjOiDEQaroqDjs0mczPK5Q",
"last_setup_error": null,
"latest_attempt": "setatt_1OGjOiDEQaroqDjsZve6lwXi",
"livemode": false,
"mandate": null,
"metadata": {},
"next_action": null,
"object": "setup_intent",
"on_behalf_of": null,
"payment_method": "pm_1OGjOiDEQaroqDjs1saPoOzz",
"payment_method_configuration_details": null,
"payment_method_options": {
"card": {
"mandate_options": null,
"network": null,
"request_three_d_secure": "automatic"
}
},
"payment_method_types": [
"card"
],
"single_use_mandate": null,
"status": "succeeded",
"usage": "off_session"
}

View File

@@ -0,0 +1,73 @@
{
"after_expiration": null,
"allow_promotion_codes": null,
"amount_subtotal": null,
"amount_total": null,
"automatic_tax": {
"enabled": false,
"status": null
},
"billing_address_collection": null,
"cancel_url": "http://zulip.testserver/upgrade/",
"client_reference_id": null,
"client_secret": null,
"consent": null,
"consent_collection": null,
"created": 1000000000,
"currency": null,
"currency_conversion": null,
"custom_fields": [],
"custom_text": {
"shipping_address": null,
"submit": null,
"terms_of_service_acceptance": null
},
"customer": "cus_NORMALIZED0001",
"customer_creation": null,
"customer_details": {
"address": null,
"email": "hamlet@zulip.com",
"name": null,
"phone": null,
"tax_exempt": null,
"tax_ids": null
},
"customer_email": null,
"expires_at": 1000000000,
"id": "cs_test_NORMALIZED01X2pCpFwoBMHMedIXu5mI7Xubvoh8wc4oenhkdkooMUfP5a",
"invoice": null,
"invoice_creation": null,
"livemode": false,
"locale": null,
"metadata": {
"type": "card_update",
"user_id": "10"
},
"mode": "setup",
"object": "checkout.session",
"payment_intent": null,
"payment_link": null,
"payment_method_collection": "always",
"payment_method_configuration_details": null,
"payment_method_options": {},
"payment_method_types": [
"card"
],
"payment_status": "no_payment_required",
"phone_number_collection": {
"enabled": false
},
"recovered_from": null,
"setup_intent": "seti_1OGjOgDEQaroqDjsnqXN4MEe",
"shipping": null,
"shipping_address_collection": null,
"shipping_options": [],
"shipping_rate": null,
"status": "open",
"submit_type": null,
"subscription": null,
"success_url": "http://zulip.testserver/billing/event_status?stripe_session_id={CHECKOUT_SESSION_ID}",
"total_details": null,
"ui_mode": "hosted",
"url": "https://checkout.stripe.com/c/pay/cs_test_NORMALIZED01X2pCpFwoBMHMedIXu5mI7Xubvoh8wc4oenhkdkooMUfP5a#fidkdWxOYHwnPyd1blpxYHZxWl1UZjFOczZJXUE2PUpzUWNPV2ZpdlFzUCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBaZmppcGhrJyknYGtkZ2lgVWlkZmBtamlhYHd2Jz9xd3BgeCUl"
}

View File

@@ -0,0 +1,80 @@
{
"data": [
{
"after_expiration": null,
"allow_promotion_codes": null,
"amount_subtotal": null,
"amount_total": null,
"automatic_tax": {
"enabled": false,
"status": null
},
"billing_address_collection": null,
"cancel_url": "http://zulip.testserver/upgrade/",
"client_reference_id": null,
"client_secret": null,
"consent": null,
"consent_collection": null,
"created": 1000000000,
"currency": null,
"currency_conversion": null,
"custom_fields": [],
"custom_text": {
"shipping_address": null,
"submit": null,
"terms_of_service_acceptance": null
},
"customer": "cus_NORMALIZED0001",
"customer_creation": null,
"customer_details": {
"address": null,
"email": "hamlet@zulip.com",
"name": null,
"phone": null,
"tax_exempt": null,
"tax_ids": null
},
"customer_email": null,
"expires_at": 1000000000,
"id": "cs_test_NORMALIZED01X2pCpFwoBMHMedIXu5mI7Xubvoh8wc4oenhkdkooMUfP5a",
"invoice": null,
"invoice_creation": null,
"livemode": false,
"locale": null,
"metadata": {
"type": "card_update",
"user_id": "10"
},
"mode": "setup",
"object": "checkout.session",
"payment_intent": null,
"payment_link": null,
"payment_method_collection": "always",
"payment_method_configuration_details": null,
"payment_method_options": {},
"payment_method_types": [
"card"
],
"payment_status": "no_payment_required",
"phone_number_collection": {
"enabled": false
},
"recovered_from": null,
"setup_intent": "seti_1OGjOgDEQaroqDjsnqXN4MEe",
"shipping": null,
"shipping_address_collection": null,
"shipping_options": [],
"shipping_rate": null,
"status": "open",
"submit_type": null,
"subscription": null,
"success_url": "http://zulip.testserver/billing/event_status?stripe_session_id={CHECKOUT_SESSION_ID}",
"total_details": null,
"ui_mode": "hosted",
"url": "https://checkout.stripe.com/c/pay/cs_test_NORMALIZED01X2pCpFwoBMHMedIXu5mI7Xubvoh8wc4oenhkdkooMUfP5a#fidkdWxOYHwnPyd1blpxYHZxWl1UZjFOczZJXUE2PUpzUWNPV2ZpdlFzUCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBaZmppcGhrJyknYGtkZ2lgVWlkZmBtamlhYHd2Jz9xd3BgeCUl"
}
],
"has_more": true,
"object": "list",
"url": "/v1/checkout/sessions"
}

View File

@@ -2839,6 +2839,115 @@ class StripeTest(StripeTestCase):
self.assertIsNone(plan.next_invoice_date) self.assertIsNone(plan.next_invoice_date)
self.assertEqual(plan.status, CustomerPlan.ENDED) self.assertEqual(plan.status, CustomerPlan.ENDED)
@mock_stripe()
def test_switch_now_free_trial_from_monthly_to_annual(self, *mocks: Mock) -> None:
user = self.example_user("hamlet")
self.login_user(user)
free_trial_end_date = self.now + timedelta(days=60)
with self.settings(FREE_TRIAL_DAYS=60):
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
self.add_card_and_upgrade(user, schedule="monthly")
plan = CustomerPlan.objects.get()
self.assertEqual(plan.next_invoice_date, free_trial_end_date)
self.assertEqual(get_realm("zulip").plan_type, Realm.PLAN_TYPE_STANDARD)
self.assertEqual(plan.status, CustomerPlan.FREE_TRIAL)
customer = get_customer_by_realm(user.realm)
assert customer is not None
result = self.client_patch(
"/json/billing/plan",
{
"status": CustomerPlan.FREE_TRIAL,
"schedule": CustomerPlan.ANNUAL,
},
)
self.assert_json_success(result)
plan.refresh_from_db()
self.assertEqual(plan.status, CustomerPlan.ENDED)
plan = CustomerPlan.objects.get(
customer=customer,
automanage_licenses=True,
price_per_license=8000,
fixed_price=None,
discount=None,
billing_cycle_anchor=self.now,
billing_schedule=CustomerPlan.ANNUAL,
invoiced_through=None,
next_invoice_date=free_trial_end_date,
tier=CustomerPlan.STANDARD,
status=CustomerPlan.FREE_TRIAL,
charge_automatically=True,
)
LicenseLedger.objects.get(
plan=plan,
is_renewal=True,
event_time=self.now,
licenses=self.seat_count,
licenses_at_next_renewal=self.seat_count,
)
realm_audit_log = RealmAuditLog.objects.filter(
event_type=RealmAuditLog.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN
).last()
assert realm_audit_log is not None
@mock_stripe()
def test_switch_now_free_trial_from_annual_to_monthly(self, *mocks: Mock) -> None:
user = self.example_user("hamlet")
self.login_user(user)
free_trial_end_date = self.now + timedelta(days=60)
with self.settings(FREE_TRIAL_DAYS=60):
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
self.add_card_and_upgrade(user, schedule="annual")
plan = CustomerPlan.objects.get()
self.assertEqual(plan.next_invoice_date, free_trial_end_date)
self.assertEqual(get_realm("zulip").plan_type, Realm.PLAN_TYPE_STANDARD)
self.assertEqual(plan.status, CustomerPlan.FREE_TRIAL)
customer = get_customer_by_realm(user.realm)
assert customer is not None
result = self.client_patch(
"/json/billing/plan",
{
"status": CustomerPlan.FREE_TRIAL,
"schedule": CustomerPlan.MONTHLY,
},
)
self.assert_json_success(result)
plan.refresh_from_db()
self.assertEqual(plan.status, CustomerPlan.ENDED)
plan = CustomerPlan.objects.get(
customer=customer,
automanage_licenses=True,
price_per_license=800,
fixed_price=None,
discount=None,
billing_cycle_anchor=self.now,
billing_schedule=CustomerPlan.MONTHLY,
invoiced_through=None,
next_invoice_date=free_trial_end_date,
tier=CustomerPlan.STANDARD,
status=CustomerPlan.FREE_TRIAL,
charge_automatically=True,
)
LicenseLedger.objects.get(
plan=plan,
is_renewal=True,
event_time=self.now,
licenses=self.seat_count,
licenses_at_next_renewal=self.seat_count,
)
realm_audit_log = RealmAuditLog.objects.filter(
event_type=RealmAuditLog.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN
).last()
assert realm_audit_log is not None
def test_end_free_trial(self) -> None: def test_end_free_trial(self) -> None:
user = self.example_user("hamlet") user = self.example_user("hamlet")

View File

@@ -110,11 +110,13 @@ def update_plan(
licenses_at_next_renewal: Optional[int] = REQ( licenses_at_next_renewal: Optional[int] = REQ(
"licenses_at_next_renewal", json_validator=check_int, default=None "licenses_at_next_renewal", json_validator=check_int, default=None
), ),
schedule: Optional[int] = REQ("schedule", json_validator=check_int, default=None),
) -> HttpResponse: ) -> HttpResponse:
update_plan_request = UpdatePlanRequest( update_plan_request = UpdatePlanRequest(
status=status, status=status,
licenses=licenses, licenses=licenses,
licenses_at_next_renewal=licenses_at_next_renewal, licenses_at_next_renewal=licenses_at_next_renewal,
schedule=schedule,
) )
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)

View File

@@ -41,7 +41,7 @@
{%if switch_to_monthly_at_end_of_cycle %}data-switch-to-monthly-eoc="true"{% endif %} {%if switch_to_monthly_at_end_of_cycle %}data-switch-to-monthly-eoc="true"{% endif %}
{%if switch_to_annual_at_end_of_cycle %}data-switch-to-annual-eoc="true"{% endif %}> {%if switch_to_annual_at_end_of_cycle %}data-switch-to-annual-eoc="true"{% endif %}>
<label for="org-billing-frequency">Billing frequency</label> <label for="org-billing-frequency">Billing frequency</label>
{% if free_trial or downgrade_at_end_of_free_trial or downgrade_at_end_of_cycle %} {% if downgrade_at_end_of_free_trial or downgrade_at_end_of_cycle %}
<div class="not-editable-realm-field"> <div class="not-editable-realm-field">
{{ billing_frequency }} {{ billing_frequency }}
</div> </div>

View File

@@ -7,6 +7,12 @@ import * as helpers from "./helpers";
const billing_frequency_schema = z.enum(["Monthly", "Annual"]); const billing_frequency_schema = z.enum(["Monthly", "Annual"]);
// Matches the CustomerPlan model in the backend.
enum BillingFrequency {
ANNUAL = 1,
MONTHLY = 2,
}
enum CustomerPlanStatus { enum CustomerPlanStatus {
ACTIVE = 1, ACTIVE = 1,
DOWNGRADE_AT_END_OF_CYCLE = 2, DOWNGRADE_AT_END_OF_CYCLE = 2,
@@ -290,24 +296,36 @@ export function initialize(): void {
$("#org-billing-frequency-confirm-button").attr("data-status", new_status); $("#org-billing-frequency-confirm-button").attr("data-status", new_status);
} else if (current_billing_frequency !== billing_frequency_selected) { } else if (current_billing_frequency !== billing_frequency_selected) {
$("#org-billing-frequency-confirm-button").toggleClass("hide", false); $("#org-billing-frequency-confirm-button").toggleClass("hide", false);
let new_status = CustomerPlanStatus.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE; let new_status = free_trial
? CustomerPlanStatus.FREE_TRIAL
: CustomerPlanStatus.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE;
let new_schedule = BillingFrequency.ANNUAL;
if (billing_frequency_selected === "Monthly") { if (billing_frequency_selected === "Monthly") {
new_status = CustomerPlanStatus.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE; new_status = free_trial
? CustomerPlanStatus.FREE_TRIAL
: CustomerPlanStatus.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE;
new_schedule = BillingFrequency.MONTHLY;
} }
$("#org-billing-frequency-confirm-button").attr("data-status", new_status); $("#org-billing-frequency-confirm-button").attr("data-status", new_status);
if (free_trial) {
// Only set schedule for free trial since it is a different process to update the frequency immediately.
$("#org-billing-frequency-confirm-button").attr("data-schedule", new_schedule);
}
} else { } else {
$("#org-billing-frequency-confirm-button").toggleClass("hide", true); $("#org-billing-frequency-confirm-button").toggleClass("hide", true);
} }
}); });
$("#org-billing-frequency-confirm-button").on("click", (e) => { $("#org-billing-frequency-confirm-button").on("click", (e) => {
const data = {
status: $("#org-billing-frequency-confirm-button").attr("data-status"),
schedule: $("#org-billing-frequency-confirm-button").attr("data-schedule"),
};
e.preventDefault(); e.preventDefault();
void $.ajax({ void $.ajax({
type: "patch", type: "patch",
url: "/json/billing/plan", url: "/json/billing/plan",
data: { data,
status: $("#org-billing-frequency-confirm-button").attr("data-status"),
},
success() { success() {
window.location.replace( window.location.replace(
"/billing/?success_message=" + "/billing/?success_message=" +