mirror of
https://github.com/zulip/zulip.git
synced 2025-11-13 02:17:19 +00:00
billing: Add frontend for upgrading by invoice.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"account_balance": 0,
|
"account_balance": 0,
|
||||||
"created": 1542524016,
|
"created": 1543088274,
|
||||||
"currency": null,
|
"currency": null,
|
||||||
"default_source": null,
|
"default_source": null,
|
||||||
"delinquent": false,
|
"delinquent": false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"account_balance": 0,
|
"account_balance": 0,
|
||||||
"created": 1542524016,
|
"created": 1543088274,
|
||||||
"currency": "usd",
|
"currency": "usd",
|
||||||
"default_source": null,
|
"default_source": null,
|
||||||
"delinquent": false,
|
"delinquent": false,
|
||||||
@@ -28,12 +28,12 @@
|
|||||||
{
|
{
|
||||||
"application_fee_percent": null,
|
"application_fee_percent": null,
|
||||||
"billing": "send_invoice",
|
"billing": "send_invoice",
|
||||||
"billing_cycle_anchor": 1542524017,
|
"billing_cycle_anchor": 1543088275,
|
||||||
"cancel_at_period_end": false,
|
"cancel_at_period_end": false,
|
||||||
"canceled_at": null,
|
"canceled_at": null,
|
||||||
"created": 1542524017,
|
"created": 1543088275,
|
||||||
"current_period_end": 1574060017,
|
"current_period_end": 1574624275,
|
||||||
"current_period_start": 1542524017,
|
"current_period_start": 1543088275,
|
||||||
"customer": "cus_NORMALIZED0001",
|
"customer": "cus_NORMALIZED0001",
|
||||||
"days_until_due": 30,
|
"days_until_due": 30,
|
||||||
"default_source": null,
|
"default_source": null,
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"created": 1542524017,
|
"created": 1543088275,
|
||||||
"id": "si_NORMALIZED0001",
|
"id": "si_NORMALIZED0001",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"object": "subscription_item",
|
"object": "subscription_item",
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
"usage_type": "licensed"
|
"usage_type": "licensed"
|
||||||
},
|
},
|
||||||
"quantity": 123,
|
"quantity": 123,
|
||||||
"start": 1542524017,
|
"start": 1543088275,
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"tax_percent": 0.0,
|
"tax_percent": 0.0,
|
||||||
"trial_end": null,
|
"trial_end": null,
|
||||||
|
|||||||
@@ -1,52 +1,8 @@
|
|||||||
{
|
{
|
||||||
"account_balance": 0,
|
"account_balance": 0,
|
||||||
"created": 1542524016,
|
"created": 1543088274,
|
||||||
"currency": "usd",
|
"currency": "usd",
|
||||||
"default_source": {
|
"default_source": null,
|
||||||
"ach_credit_transfer": {
|
|
||||||
"account_number": "test_c61c954f0ca8",
|
|
||||||
"bank_name": "TEST BANK",
|
|
||||||
"fingerprint": "NORMALIZED000001",
|
|
||||||
"refund_account_holder_name": null,
|
|
||||||
"refund_account_holder_type": null,
|
|
||||||
"refund_account_number": null,
|
|
||||||
"refund_routing_number": null,
|
|
||||||
"routing_number": "110000000",
|
|
||||||
"swift_code": "TSTEZ122"
|
|
||||||
},
|
|
||||||
"amount": null,
|
|
||||||
"client_secret": "src_client_secret_DzjdyQ9DrS7xsDJprt8u1VGU",
|
|
||||||
"created": 1542524018,
|
|
||||||
"currency": "usd",
|
|
||||||
"customer": "cus_NORMALIZED0001",
|
|
||||||
"flow": "receiver",
|
|
||||||
"id": "src_1DXkA2Gh0CmXqmnwvjBfz4VX",
|
|
||||||
"livemode": false,
|
|
||||||
"metadata": {},
|
|
||||||
"object": "source",
|
|
||||||
"owner": {
|
|
||||||
"address": null,
|
|
||||||
"email": "amount_0@stripe.com",
|
|
||||||
"name": null,
|
|
||||||
"phone": null,
|
|
||||||
"verified_address": null,
|
|
||||||
"verified_email": null,
|
|
||||||
"verified_name": null,
|
|
||||||
"verified_phone": null
|
|
||||||
},
|
|
||||||
"receiver": {
|
|
||||||
"address": "110000000-test_c61c954f0ca8",
|
|
||||||
"amount_charged": 0,
|
|
||||||
"amount_received": 0,
|
|
||||||
"amount_returned": 0,
|
|
||||||
"refund_attributes_method": "email",
|
|
||||||
"refund_attributes_status": "missing"
|
|
||||||
},
|
|
||||||
"statement_descriptor": null,
|
|
||||||
"status": "pending",
|
|
||||||
"type": "ach_credit_transfer",
|
|
||||||
"usage": "reusable"
|
|
||||||
},
|
|
||||||
"delinquent": false,
|
"delinquent": false,
|
||||||
"description": "zulip (Zulip Dev)",
|
"description": "zulip (Zulip Dev)",
|
||||||
"discount": null,
|
"discount": null,
|
||||||
@@ -61,56 +17,10 @@
|
|||||||
"object": "customer",
|
"object": "customer",
|
||||||
"shipping": null,
|
"shipping": null,
|
||||||
"sources": {
|
"sources": {
|
||||||
"data": [
|
"data": [],
|
||||||
{
|
|
||||||
"ach_credit_transfer": {
|
|
||||||
"account_number": "test_c61c954f0ca8",
|
|
||||||
"bank_name": "TEST BANK",
|
|
||||||
"fingerprint": "NORMALIZED000001",
|
|
||||||
"refund_account_holder_name": null,
|
|
||||||
"refund_account_holder_type": null,
|
|
||||||
"refund_account_number": null,
|
|
||||||
"refund_routing_number": null,
|
|
||||||
"routing_number": "110000000",
|
|
||||||
"swift_code": "TSTEZ122"
|
|
||||||
},
|
|
||||||
"amount": null,
|
|
||||||
"client_secret": "src_client_secret_DzjdyQ9DrS7xsDJprt8u1VGU",
|
|
||||||
"created": 1542524018,
|
|
||||||
"currency": "usd",
|
|
||||||
"customer": "cus_NORMALIZED0001",
|
|
||||||
"flow": "receiver",
|
|
||||||
"id": "src_1DXkA2Gh0CmXqmnwvjBfz4VX",
|
|
||||||
"livemode": false,
|
|
||||||
"metadata": {},
|
|
||||||
"object": "source",
|
|
||||||
"owner": {
|
|
||||||
"address": null,
|
|
||||||
"email": "amount_0@stripe.com",
|
|
||||||
"name": null,
|
|
||||||
"phone": null,
|
|
||||||
"verified_address": null,
|
|
||||||
"verified_email": null,
|
|
||||||
"verified_name": null,
|
|
||||||
"verified_phone": null
|
|
||||||
},
|
|
||||||
"receiver": {
|
|
||||||
"address": "110000000-test_c61c954f0ca8",
|
|
||||||
"amount_charged": 0,
|
|
||||||
"amount_received": 0,
|
|
||||||
"amount_returned": 0,
|
|
||||||
"refund_attributes_method": "email",
|
|
||||||
"refund_attributes_status": "missing"
|
|
||||||
},
|
|
||||||
"statement_descriptor": null,
|
|
||||||
"status": "pending",
|
|
||||||
"type": "ach_credit_transfer",
|
|
||||||
"usage": "reusable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"has_more": false,
|
"has_more": false,
|
||||||
"object": "list",
|
"object": "list",
|
||||||
"total_count": 1,
|
"total_count": 0,
|
||||||
"url": "/v1/customers/cus_NORMALIZED0001/sources"
|
"url": "/v1/customers/cus_NORMALIZED0001/sources"
|
||||||
},
|
},
|
||||||
"subscriptions": {
|
"subscriptions": {
|
||||||
@@ -118,12 +28,12 @@
|
|||||||
{
|
{
|
||||||
"application_fee_percent": null,
|
"application_fee_percent": null,
|
||||||
"billing": "send_invoice",
|
"billing": "send_invoice",
|
||||||
"billing_cycle_anchor": 1542524017,
|
"billing_cycle_anchor": 1543088275,
|
||||||
"cancel_at_period_end": false,
|
"cancel_at_period_end": false,
|
||||||
"canceled_at": null,
|
"canceled_at": null,
|
||||||
"created": 1542524017,
|
"created": 1543088275,
|
||||||
"current_period_end": 1574060017,
|
"current_period_end": 1574624275,
|
||||||
"current_period_start": 1542524017,
|
"current_period_start": 1543088275,
|
||||||
"customer": "cus_NORMALIZED0001",
|
"customer": "cus_NORMALIZED0001",
|
||||||
"days_until_due": 30,
|
"days_until_due": 30,
|
||||||
"default_source": null,
|
"default_source": null,
|
||||||
@@ -133,7 +43,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"created": 1542524017,
|
"created": 1543088275,
|
||||||
"id": "si_NORMALIZED0001",
|
"id": "si_NORMALIZED0001",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"object": "subscription_item",
|
"object": "subscription_item",
|
||||||
@@ -192,7 +102,7 @@
|
|||||||
"usage_type": "licensed"
|
"usage_type": "licensed"
|
||||||
},
|
},
|
||||||
"quantity": 8,
|
"quantity": 8,
|
||||||
"start": 1542524018,
|
"start": 1543088276,
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"tax_percent": 0.0,
|
"tax_percent": 0.0,
|
||||||
"trial_end": null,
|
"trial_end": null,
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
{
|
||||||
|
"account_balance": 0,
|
||||||
|
"created": 1543088274,
|
||||||
|
"currency": "usd",
|
||||||
|
"default_source": {
|
||||||
|
"ach_credit_transfer": {
|
||||||
|
"account_number": "test_1ba48fb022d9",
|
||||||
|
"bank_name": "TEST BANK",
|
||||||
|
"fingerprint": "NORMALIZED000001",
|
||||||
|
"refund_account_holder_name": null,
|
||||||
|
"refund_account_holder_type": null,
|
||||||
|
"refund_account_number": null,
|
||||||
|
"refund_routing_number": null,
|
||||||
|
"routing_number": "110000000",
|
||||||
|
"swift_code": "TSTEZ122"
|
||||||
|
},
|
||||||
|
"amount": null,
|
||||||
|
"client_secret": "src_client_secret_E2BJZG7aBRFFRLMIJxGPF0b5",
|
||||||
|
"created": 1543088277,
|
||||||
|
"currency": "usd",
|
||||||
|
"customer": "cus_NORMALIZED0001",
|
||||||
|
"flow": "receiver",
|
||||||
|
"id": "src_1Da6wzGh0CmXqmnw6RH4WCG7",
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"object": "source",
|
||||||
|
"owner": {
|
||||||
|
"address": null,
|
||||||
|
"email": "amount_0@stripe.com",
|
||||||
|
"name": null,
|
||||||
|
"phone": null,
|
||||||
|
"verified_address": null,
|
||||||
|
"verified_email": null,
|
||||||
|
"verified_name": null,
|
||||||
|
"verified_phone": null
|
||||||
|
},
|
||||||
|
"receiver": {
|
||||||
|
"address": "110000000-test_1ba48fb022d9",
|
||||||
|
"amount_charged": 0,
|
||||||
|
"amount_received": 0,
|
||||||
|
"amount_returned": 0,
|
||||||
|
"refund_attributes_method": "email",
|
||||||
|
"refund_attributes_status": "missing"
|
||||||
|
},
|
||||||
|
"statement_descriptor": null,
|
||||||
|
"status": "pending",
|
||||||
|
"type": "ach_credit_transfer",
|
||||||
|
"usage": "reusable"
|
||||||
|
},
|
||||||
|
"delinquent": false,
|
||||||
|
"description": "zulip (Zulip Dev)",
|
||||||
|
"discount": null,
|
||||||
|
"email": "hamlet@zulip.com",
|
||||||
|
"id": "cus_NORMALIZED0001",
|
||||||
|
"invoice_prefix": "NORMA01",
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {
|
||||||
|
"realm_id": "1",
|
||||||
|
"realm_str": "zulip"
|
||||||
|
},
|
||||||
|
"object": "customer",
|
||||||
|
"shipping": null,
|
||||||
|
"sources": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"ach_credit_transfer": {
|
||||||
|
"account_number": "test_1ba48fb022d9",
|
||||||
|
"bank_name": "TEST BANK",
|
||||||
|
"fingerprint": "NORMALIZED000001",
|
||||||
|
"refund_account_holder_name": null,
|
||||||
|
"refund_account_holder_type": null,
|
||||||
|
"refund_account_number": null,
|
||||||
|
"refund_routing_number": null,
|
||||||
|
"routing_number": "110000000",
|
||||||
|
"swift_code": "TSTEZ122"
|
||||||
|
},
|
||||||
|
"amount": null,
|
||||||
|
"client_secret": "src_client_secret_E2BJZG7aBRFFRLMIJxGPF0b5",
|
||||||
|
"created": 1543088277,
|
||||||
|
"currency": "usd",
|
||||||
|
"customer": "cus_NORMALIZED0001",
|
||||||
|
"flow": "receiver",
|
||||||
|
"id": "src_1Da6wzGh0CmXqmnw6RH4WCG7",
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"object": "source",
|
||||||
|
"owner": {
|
||||||
|
"address": null,
|
||||||
|
"email": "amount_0@stripe.com",
|
||||||
|
"name": null,
|
||||||
|
"phone": null,
|
||||||
|
"verified_address": null,
|
||||||
|
"verified_email": null,
|
||||||
|
"verified_name": null,
|
||||||
|
"verified_phone": null
|
||||||
|
},
|
||||||
|
"receiver": {
|
||||||
|
"address": "110000000-test_1ba48fb022d9",
|
||||||
|
"amount_charged": 0,
|
||||||
|
"amount_received": 0,
|
||||||
|
"amount_returned": 0,
|
||||||
|
"refund_attributes_method": "email",
|
||||||
|
"refund_attributes_status": "missing"
|
||||||
|
},
|
||||||
|
"statement_descriptor": null,
|
||||||
|
"status": "pending",
|
||||||
|
"type": "ach_credit_transfer",
|
||||||
|
"usage": "reusable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"has_more": false,
|
||||||
|
"object": "list",
|
||||||
|
"total_count": 1,
|
||||||
|
"url": "/v1/customers/cus_NORMALIZED0001/sources"
|
||||||
|
},
|
||||||
|
"subscriptions": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"application_fee_percent": null,
|
||||||
|
"billing": "send_invoice",
|
||||||
|
"billing_cycle_anchor": 1543088275,
|
||||||
|
"cancel_at_period_end": false,
|
||||||
|
"canceled_at": null,
|
||||||
|
"created": 1543088275,
|
||||||
|
"current_period_end": 1574624275,
|
||||||
|
"current_period_start": 1543088275,
|
||||||
|
"customer": "cus_NORMALIZED0001",
|
||||||
|
"days_until_due": 30,
|
||||||
|
"default_source": null,
|
||||||
|
"discount": null,
|
||||||
|
"ended_at": null,
|
||||||
|
"id": "sub_NORMALIZED0001",
|
||||||
|
"items": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"created": 1543088275,
|
||||||
|
"id": "si_NORMALIZED0001",
|
||||||
|
"metadata": {},
|
||||||
|
"object": "subscription_item",
|
||||||
|
"plan": {
|
||||||
|
"active": true,
|
||||||
|
"aggregate_usage": null,
|
||||||
|
"amount": 8000,
|
||||||
|
"billing_scheme": "per_unit",
|
||||||
|
"created": 1539831971,
|
||||||
|
"currency": "usd",
|
||||||
|
"id": "plan_Do3xCvbzO89OsR",
|
||||||
|
"interval": "year",
|
||||||
|
"interval_count": 1,
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"nickname": "annual",
|
||||||
|
"object": "plan",
|
||||||
|
"product": "prod_Do3x494SetTDpx",
|
||||||
|
"tiers": null,
|
||||||
|
"tiers_mode": null,
|
||||||
|
"transform_usage": null,
|
||||||
|
"trial_period_days": null,
|
||||||
|
"usage_type": "licensed"
|
||||||
|
},
|
||||||
|
"quantity": 8,
|
||||||
|
"subscription": "sub_NORMALIZED0001"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"has_more": false,
|
||||||
|
"object": "list",
|
||||||
|
"total_count": 1,
|
||||||
|
"url": "/v1/subscription_items?subscription=sub_NORMALIZED0001"
|
||||||
|
},
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"object": "subscription",
|
||||||
|
"plan": {
|
||||||
|
"active": true,
|
||||||
|
"aggregate_usage": null,
|
||||||
|
"amount": 8000,
|
||||||
|
"billing_scheme": "per_unit",
|
||||||
|
"created": 1539831971,
|
||||||
|
"currency": "usd",
|
||||||
|
"id": "plan_Do3xCvbzO89OsR",
|
||||||
|
"interval": "year",
|
||||||
|
"interval_count": 1,
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"nickname": "annual",
|
||||||
|
"object": "plan",
|
||||||
|
"product": "prod_Do3x494SetTDpx",
|
||||||
|
"tiers": null,
|
||||||
|
"tiers_mode": null,
|
||||||
|
"transform_usage": null,
|
||||||
|
"trial_period_days": null,
|
||||||
|
"usage_type": "licensed"
|
||||||
|
},
|
||||||
|
"quantity": 8,
|
||||||
|
"start": 1543088276,
|
||||||
|
"status": "active",
|
||||||
|
"tax_percent": 0.0,
|
||||||
|
"trial_end": null,
|
||||||
|
"trial_start": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"has_more": false,
|
||||||
|
"object": "list",
|
||||||
|
"total_count": 1,
|
||||||
|
"url": "/v1/customers/cus_NORMALIZED0001/subscriptions"
|
||||||
|
},
|
||||||
|
"tax_info": null,
|
||||||
|
"tax_info_verification": null
|
||||||
|
}
|
||||||
@@ -13,15 +13,15 @@
|
|||||||
"charge": null,
|
"charge": null,
|
||||||
"currency": "usd",
|
"currency": "usd",
|
||||||
"customer": "cus_NORMALIZED0001",
|
"customer": "cus_NORMALIZED0001",
|
||||||
"date": 1542524017,
|
"date": 1543088275,
|
||||||
"default_source": null,
|
"default_source": null,
|
||||||
"description": "",
|
"description": "",
|
||||||
"discount": null,
|
"discount": null,
|
||||||
"due_date": 1545116017,
|
"due_date": 1545680275,
|
||||||
"ending_balance": null,
|
"ending_balance": null,
|
||||||
"finalized_at": null,
|
"finalized_at": null,
|
||||||
"hosted_invoice_url": null,
|
"hosted_invoice_url": null,
|
||||||
"id": "in_1DXkA1Gh0CmXqmnwNApgkOR1",
|
"id": "in_1Da6wxGh0CmXqmnwfILsY1kf",
|
||||||
"invoice_pdf": null,
|
"invoice_pdf": null,
|
||||||
"lines": {
|
"lines": {
|
||||||
"data": [
|
"data": [
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"object": "line_item",
|
"object": "line_item",
|
||||||
"period": {
|
"period": {
|
||||||
"end": 1574060017,
|
"end": 1574624275,
|
||||||
"start": 1542524017
|
"start": 1543088275
|
||||||
},
|
},
|
||||||
"plan": {
|
"plan": {
|
||||||
"active": true,
|
"active": true,
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"has_more": false,
|
"has_more": false,
|
||||||
"object": "list",
|
"object": "list",
|
||||||
"total_count": 1,
|
"total_count": 1,
|
||||||
"url": "/v1/invoices/in_1DXkA1Gh0CmXqmnwNApgkOR1/lines"
|
"url": "/v1/invoices/in_1Da6wxGh0CmXqmnwfILsY1kf/lines"
|
||||||
},
|
},
|
||||||
"livemode": false,
|
"livemode": false,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
@@ -78,8 +78,8 @@
|
|||||||
"object": "invoice",
|
"object": "invoice",
|
||||||
"paid": false,
|
"paid": false,
|
||||||
"payment_intent": null,
|
"payment_intent": null,
|
||||||
"period_end": 1542524017,
|
"period_end": 1543088275,
|
||||||
"period_start": 1542524017,
|
"period_start": 1543088275,
|
||||||
"receipt_number": null,
|
"receipt_number": null,
|
||||||
"starting_balance": 0,
|
"starting_balance": 0,
|
||||||
"statement_descriptor": null,
|
"statement_descriptor": null,
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
"tax": 0,
|
"tax": 0,
|
||||||
"tax_percent": 0.0,
|
"tax_percent": 0.0,
|
||||||
"total": 984000,
|
"total": 984000,
|
||||||
"webhooks_delivered_at": 1542524017
|
"webhooks_delivered_at": 1543088275
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_more": false,
|
"has_more": false,
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
{
|
||||||
|
"amount_due": 0,
|
||||||
|
"amount_paid": 0,
|
||||||
|
"amount_remaining": 0,
|
||||||
|
"application_fee": null,
|
||||||
|
"attempt_count": 0,
|
||||||
|
"attempted": false,
|
||||||
|
"billing": "send_invoice",
|
||||||
|
"billing_reason": "upcoming",
|
||||||
|
"charge": null,
|
||||||
|
"currency": "usd",
|
||||||
|
"customer": "cus_NORMALIZED0001",
|
||||||
|
"date": 1574624275,
|
||||||
|
"default_source": null,
|
||||||
|
"description": "",
|
||||||
|
"discount": null,
|
||||||
|
"due_date": 1577216275,
|
||||||
|
"ending_balance": -856000,
|
||||||
|
"finalized_at": null,
|
||||||
|
"lines": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"amount": -984000,
|
||||||
|
"currency": "usd",
|
||||||
|
"description": "Unused time on 123 \u00d7 Zulip Cloud Premium after 24 Nov 2018",
|
||||||
|
"discountable": false,
|
||||||
|
"id": "ii_1Da6wyGh0CmXqmnwgou1s917",
|
||||||
|
"invoice_item": "ii_1Da6wyGh0CmXqmnwgou1s917",
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"object": "line_item",
|
||||||
|
"period": {
|
||||||
|
"end": 1574624275,
|
||||||
|
"start": 1543088275
|
||||||
|
},
|
||||||
|
"plan": {
|
||||||
|
"active": true,
|
||||||
|
"aggregate_usage": null,
|
||||||
|
"amount": 8000,
|
||||||
|
"billing_scheme": "per_unit",
|
||||||
|
"created": 1539831971,
|
||||||
|
"currency": "usd",
|
||||||
|
"id": "plan_Do3xCvbzO89OsR",
|
||||||
|
"interval": "year",
|
||||||
|
"interval_count": 1,
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"nickname": "annual",
|
||||||
|
"object": "plan",
|
||||||
|
"product": "prod_Do3x494SetTDpx",
|
||||||
|
"tiers": null,
|
||||||
|
"tiers_mode": null,
|
||||||
|
"transform_usage": null,
|
||||||
|
"trial_period_days": null,
|
||||||
|
"usage_type": "licensed"
|
||||||
|
},
|
||||||
|
"proration": true,
|
||||||
|
"quantity": 123,
|
||||||
|
"subscription": "sub_NORMALIZED0001",
|
||||||
|
"subscription_item": "si_NORMALIZED0001",
|
||||||
|
"type": "invoiceitem"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": 64000,
|
||||||
|
"currency": "usd",
|
||||||
|
"description": "Remaining time on 8 \u00d7 Zulip Cloud Premium after 24 Nov 2018",
|
||||||
|
"discountable": false,
|
||||||
|
"id": "ii_1Da6wyGh0CmXqmnwKQBPI88u",
|
||||||
|
"invoice_item": "ii_1Da6wyGh0CmXqmnwKQBPI88u",
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"object": "line_item",
|
||||||
|
"period": {
|
||||||
|
"end": 1574624275,
|
||||||
|
"start": 1543088275
|
||||||
|
},
|
||||||
|
"plan": {
|
||||||
|
"active": true,
|
||||||
|
"aggregate_usage": null,
|
||||||
|
"amount": 8000,
|
||||||
|
"billing_scheme": "per_unit",
|
||||||
|
"created": 1539831971,
|
||||||
|
"currency": "usd",
|
||||||
|
"id": "plan_Do3xCvbzO89OsR",
|
||||||
|
"interval": "year",
|
||||||
|
"interval_count": 1,
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"nickname": "annual",
|
||||||
|
"object": "plan",
|
||||||
|
"product": "prod_Do3x494SetTDpx",
|
||||||
|
"tiers": null,
|
||||||
|
"tiers_mode": null,
|
||||||
|
"transform_usage": null,
|
||||||
|
"trial_period_days": null,
|
||||||
|
"usage_type": "licensed"
|
||||||
|
},
|
||||||
|
"proration": true,
|
||||||
|
"quantity": 8,
|
||||||
|
"subscription": "sub_NORMALIZED0001",
|
||||||
|
"subscription_item": "si_NORMALIZED0001",
|
||||||
|
"type": "invoiceitem"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": 64000,
|
||||||
|
"currency": "usd",
|
||||||
|
"description": "8 user \u00d7 Zulip Cloud Premium (at $80.00 / year)",
|
||||||
|
"discountable": true,
|
||||||
|
"id": "sli_NORMALIZED0001",
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"object": "line_item",
|
||||||
|
"period": {
|
||||||
|
"end": 1606246675,
|
||||||
|
"start": 1574624275
|
||||||
|
},
|
||||||
|
"plan": {
|
||||||
|
"active": true,
|
||||||
|
"aggregate_usage": null,
|
||||||
|
"amount": 8000,
|
||||||
|
"billing_scheme": "per_unit",
|
||||||
|
"created": 1539831971,
|
||||||
|
"currency": "usd",
|
||||||
|
"id": "plan_Do3xCvbzO89OsR",
|
||||||
|
"interval": "year",
|
||||||
|
"interval_count": 1,
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"nickname": "annual",
|
||||||
|
"object": "plan",
|
||||||
|
"product": "prod_Do3x494SetTDpx",
|
||||||
|
"tiers": null,
|
||||||
|
"tiers_mode": null,
|
||||||
|
"transform_usage": null,
|
||||||
|
"trial_period_days": null,
|
||||||
|
"usage_type": "licensed"
|
||||||
|
},
|
||||||
|
"proration": false,
|
||||||
|
"quantity": 8,
|
||||||
|
"subscription": "sub_NORMALIZED0001",
|
||||||
|
"subscription_item": "si_NORMALIZED0001",
|
||||||
|
"type": "subscription"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"has_more": false,
|
||||||
|
"object": "list",
|
||||||
|
"total_count": 3,
|
||||||
|
"url": "/v1/invoices/upcoming/lines?customer=cus_NORMALIZED0001"
|
||||||
|
},
|
||||||
|
"livemode": false,
|
||||||
|
"metadata": {},
|
||||||
|
"next_payment_attempt": null,
|
||||||
|
"number": "NORMALI-0002",
|
||||||
|
"object": "invoice",
|
||||||
|
"paid": false,
|
||||||
|
"payment_intent": null,
|
||||||
|
"period_end": 1574624275,
|
||||||
|
"period_start": 1543088275,
|
||||||
|
"receipt_number": null,
|
||||||
|
"starting_balance": 0,
|
||||||
|
"statement_descriptor": null,
|
||||||
|
"status": "draft",
|
||||||
|
"subscription": "sub_NORMALIZED0001",
|
||||||
|
"subtotal": -856000,
|
||||||
|
"tax": 0,
|
||||||
|
"tax_percent": 0.0,
|
||||||
|
"total": -856000,
|
||||||
|
"webhooks_delivered_at": null
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"application_fee_percent": null,
|
"application_fee_percent": null,
|
||||||
"billing": "send_invoice",
|
"billing": "send_invoice",
|
||||||
"billing_cycle_anchor": 1542524017,
|
"billing_cycle_anchor": 1543088275,
|
||||||
"cancel_at_period_end": false,
|
"cancel_at_period_end": false,
|
||||||
"canceled_at": null,
|
"canceled_at": null,
|
||||||
"created": 1542524017,
|
"created": 1543088275,
|
||||||
"current_period_end": 1574060017,
|
"current_period_end": 1574624275,
|
||||||
"current_period_start": 1542524017,
|
"current_period_start": 1543088275,
|
||||||
"customer": "cus_NORMALIZED0001",
|
"customer": "cus_NORMALIZED0001",
|
||||||
"days_until_due": 30,
|
"days_until_due": 30,
|
||||||
"default_source": null,
|
"default_source": null,
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"created": 1542524017,
|
"created": 1543088275,
|
||||||
"id": "si_NORMALIZED0001",
|
"id": "si_NORMALIZED0001",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"object": "subscription_item",
|
"object": "subscription_item",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
"usage_type": "licensed"
|
"usage_type": "licensed"
|
||||||
},
|
},
|
||||||
"quantity": 123,
|
"quantity": 123,
|
||||||
"start": 1542524017,
|
"start": 1543088275,
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"tax_percent": 0.0,
|
"tax_percent": 0.0,
|
||||||
"trial_end": null,
|
"trial_end": null,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"application_fee_percent": null,
|
"application_fee_percent": null,
|
||||||
"billing": "send_invoice",
|
"billing": "send_invoice",
|
||||||
"billing_cycle_anchor": 1542524017,
|
"billing_cycle_anchor": 1543088275,
|
||||||
"cancel_at_period_end": false,
|
"cancel_at_period_end": false,
|
||||||
"canceled_at": null,
|
"canceled_at": null,
|
||||||
"created": 1542524017,
|
"created": 1543088275,
|
||||||
"current_period_end": 1574060017,
|
"current_period_end": 1574624275,
|
||||||
"current_period_start": 1542524017,
|
"current_period_start": 1543088275,
|
||||||
"customer": "cus_NORMALIZED0001",
|
"customer": "cus_NORMALIZED0001",
|
||||||
"days_until_due": 30,
|
"days_until_due": 30,
|
||||||
"default_source": null,
|
"default_source": null,
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"created": 1542524017,
|
"created": 1543088275,
|
||||||
"id": "si_NORMALIZED0001",
|
"id": "si_NORMALIZED0001",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"object": "subscription_item",
|
"object": "subscription_item",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
"usage_type": "licensed"
|
"usage_type": "licensed"
|
||||||
},
|
},
|
||||||
"quantity": 8,
|
"quantity": 8,
|
||||||
"start": 1542524018,
|
"start": 1543088276,
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"tax_percent": 0.0,
|
"tax_percent": 0.0,
|
||||||
"trial_end": null,
|
"trial_end": null,
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ class StripeTest(ZulipTestCase):
|
|||||||
user = self.example_user("hamlet")
|
user = self.example_user("hamlet")
|
||||||
self.login(user.email)
|
self.login(user.email)
|
||||||
response = self.client_get("/upgrade/")
|
response = self.client_get("/upgrade/")
|
||||||
self.assert_in_success_response(['We can also bill by invoice'], response)
|
self.assert_in_success_response(['Pay annually'], response)
|
||||||
self.assertFalse(user.realm.has_seat_based_plan)
|
self.assertFalse(user.realm.has_seat_based_plan)
|
||||||
self.assertNotEqual(user.realm.plan_type, Realm.STANDARD)
|
self.assertNotEqual(user.realm.plan_type, Realm.STANDARD)
|
||||||
self.assertFalse(Customer.objects.filter(realm=user.realm).exists())
|
self.assertFalse(Customer.objects.filter(realm=user.realm).exists())
|
||||||
@@ -331,9 +331,9 @@ class StripeTest(ZulipTestCase):
|
|||||||
|
|
||||||
# Check /billing has the correct information
|
# Check /billing has the correct information
|
||||||
response = self.client_get("/billing/")
|
response = self.client_get("/billing/")
|
||||||
self.assert_not_in_success_response(['We can also bill by invoice'], response)
|
self.assert_not_in_success_response(['Pay annually'], response)
|
||||||
for substring in ['Your plan will renew on', '$%s.00' % (80 * self.quantity,),
|
for substring in ['Your plan will renew on', '$%s.00' % (80 * self.quantity,),
|
||||||
'Card ending in 4242']:
|
'Card ending in 4242', 'Update card']:
|
||||||
self.assert_in_response(substring, response)
|
self.assert_in_response(substring, response)
|
||||||
|
|
||||||
@mock_stripe("Token.create", "Invoice.upcoming", "Customer.retrieve", "Customer.create", "Subscription.create")
|
@mock_stripe("Token.create", "Invoice.upcoming", "Customer.retrieve", "Customer.create", "Subscription.create")
|
||||||
@@ -474,6 +474,7 @@ class StripeTest(ZulipTestCase):
|
|||||||
self.assertEqual(response['error_description'], 'tampered seat count')
|
self.assertEqual(response['error_description'], 'tampered seat count')
|
||||||
|
|
||||||
def test_upgrade_with_tampered_plan(self) -> None:
|
def test_upgrade_with_tampered_plan(self) -> None:
|
||||||
|
# Test with an unknown plan
|
||||||
self.login(self.example_email("hamlet"))
|
self.login(self.example_email("hamlet"))
|
||||||
response = self.client_post("/upgrade/", {
|
response = self.client_post("/upgrade/", {
|
||||||
'stripeToken': self.token,
|
'stripeToken': self.token,
|
||||||
@@ -484,6 +485,16 @@ class StripeTest(ZulipTestCase):
|
|||||||
})
|
})
|
||||||
self.assert_in_success_response(["Upgrade to Zulip Standard"], response)
|
self.assert_in_success_response(["Upgrade to Zulip Standard"], response)
|
||||||
self.assertEqual(response['error_description'], 'tampered plan')
|
self.assertEqual(response['error_description'], 'tampered plan')
|
||||||
|
# Test with a plan that's valid, but not if you're paying by invoice
|
||||||
|
response = self.client_post("/upgrade/", {
|
||||||
|
'invoiced_seat_count': 123,
|
||||||
|
'signed_seat_count': self.signed_seat_count,
|
||||||
|
'salt': self.salt,
|
||||||
|
'plan': Plan.CLOUD_MONTHLY,
|
||||||
|
'billing_modality': 'send_invoice',
|
||||||
|
})
|
||||||
|
self.assert_in_success_response(["Upgrade to Zulip Standard"], response)
|
||||||
|
self.assertEqual(response['error_description'], 'tampered plan')
|
||||||
|
|
||||||
def test_upgrade_with_insufficient_invoiced_seat_count(self) -> None:
|
def test_upgrade_with_insufficient_invoiced_seat_count(self) -> None:
|
||||||
self.login(self.example_email("hamlet"))
|
self.login(self.example_email("hamlet"))
|
||||||
@@ -510,6 +521,16 @@ class StripeTest(ZulipTestCase):
|
|||||||
self.assert_in_success_response(["Upgrade to Zulip Standard",
|
self.assert_in_success_response(["Upgrade to Zulip Standard",
|
||||||
"at least %d users" % (self.quantity,)], response)
|
"at least %d users" % (self.quantity,)], response)
|
||||||
self.assertEqual(response['error_description'], 'lowball seat count')
|
self.assertEqual(response['error_description'], 'lowball seat count')
|
||||||
|
# Test not setting an invoiced_seat_count
|
||||||
|
response = self.client_post("/upgrade/", {
|
||||||
|
'signed_seat_count': self.signed_seat_count,
|
||||||
|
'salt': self.salt,
|
||||||
|
'plan': Plan.CLOUD_ANNUAL,
|
||||||
|
'billing_modality': 'send_invoice',
|
||||||
|
})
|
||||||
|
self.assert_in_success_response(["Upgrade to Zulip Standard",
|
||||||
|
"at least %d users" % (MIN_INVOICED_SEAT_COUNT,)], response)
|
||||||
|
self.assertEqual(response['error_description'], 'lowball seat count')
|
||||||
|
|
||||||
@patch("corporate.lib.stripe.billing_logger.error")
|
@patch("corporate.lib.stripe.billing_logger.error")
|
||||||
def test_upgrade_with_uncaught_exception(self, mock1: Mock) -> None:
|
def test_upgrade_with_uncaught_exception(self, mock1: Mock) -> None:
|
||||||
@@ -527,8 +548,8 @@ class StripeTest(ZulipTestCase):
|
|||||||
self.assertEqual(response['error_description'], 'uncaught exception during upgrade')
|
self.assertEqual(response['error_description'], 'uncaught exception during upgrade')
|
||||||
|
|
||||||
@mock_stripe("Customer.create", "Subscription.create", "Subscription.save",
|
@mock_stripe("Customer.create", "Subscription.create", "Subscription.save",
|
||||||
"Customer.retrieve", "Invoice.list")
|
"Customer.retrieve", "Invoice.list", "Invoice.upcoming")
|
||||||
def test_upgrade_billing_by_invoice(self, mock5: Mock, mock4: Mock, mock3: Mock,
|
def test_upgrade_billing_by_invoice(self, mock6: Mock, mock5: Mock, mock4: Mock, mock3: Mock,
|
||||||
mock2: Mock, mock1: Mock) -> None:
|
mock2: Mock, mock1: Mock) -> None:
|
||||||
user = self.example_user("hamlet")
|
user = self.example_user("hamlet")
|
||||||
self.login(user.email)
|
self.login(user.email)
|
||||||
@@ -590,6 +611,12 @@ class StripeTest(ZulipTestCase):
|
|||||||
event_type=RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET).values_list('extra_data', flat=True).first()),
|
event_type=RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET).values_list('extra_data', flat=True).first()),
|
||||||
{'quantity': self.quantity})
|
{'quantity': self.quantity})
|
||||||
|
|
||||||
|
# Check /billing has the correct information
|
||||||
|
response = self.client_get("/billing/")
|
||||||
|
self.assert_not_in_success_response(['Pay annually', 'Update card'], response)
|
||||||
|
for substring in ['Your plan will renew on', 'Billed by invoice']:
|
||||||
|
self.assert_in_response(substring, response)
|
||||||
|
|
||||||
@patch("stripe.Customer.retrieve", side_effect=mock_customer_with_subscription)
|
@patch("stripe.Customer.retrieve", side_effect=mock_customer_with_subscription)
|
||||||
def test_redirect_for_billing_home(self, mock_customer_with_subscription: Mock) -> None:
|
def test_redirect_for_billing_home(self, mock_customer_with_subscription: Mock) -> None:
|
||||||
user = self.example_user("iago")
|
user = self.example_user("iago")
|
||||||
@@ -612,7 +639,7 @@ class StripeTest(ZulipTestCase):
|
|||||||
|
|
||||||
with patch("corporate.views.upcoming_invoice_total", return_value=0):
|
with patch("corporate.views.upcoming_invoice_total", return_value=0):
|
||||||
response = self.client_get("/billing/")
|
response = self.client_get("/billing/")
|
||||||
self.assert_not_in_success_response(['We can also bill by invoice'], response)
|
self.assert_not_in_success_response(['Pay annually'], response)
|
||||||
self.assert_in_response('Your plan will renew on', response)
|
self.assert_in_response('Your plan will renew on', response)
|
||||||
|
|
||||||
def test_get_seat_count(self) -> None:
|
def test_get_seat_count(self) -> None:
|
||||||
|
|||||||
@@ -20,14 +20,19 @@ from corporate.lib.stripe import STRIPE_PUBLISHABLE_KEY, \
|
|||||||
stripe_get_customer, upcoming_invoice_total, get_seat_count, \
|
stripe_get_customer, upcoming_invoice_total, get_seat_count, \
|
||||||
extract_current_subscription, process_initial_upgrade, sign_string, \
|
extract_current_subscription, process_initial_upgrade, sign_string, \
|
||||||
unsign_string, BillingError, process_downgrade, do_replace_payment_source, \
|
unsign_string, BillingError, process_downgrade, do_replace_payment_source, \
|
||||||
MIN_INVOICED_SEAT_COUNT
|
MIN_INVOICED_SEAT_COUNT, DEFAULT_INVOICE_DAYS_UNTIL_DUE
|
||||||
from corporate.models import Customer, Plan
|
from corporate.models import Customer, Plan
|
||||||
|
|
||||||
billing_logger = logging.getLogger('corporate.stripe')
|
billing_logger = logging.getLogger('corporate.stripe')
|
||||||
|
|
||||||
def unsign_and_check_upgrade_parameters(user: UserProfile, plan_nickname: str,
|
def unsign_and_check_upgrade_parameters(user: UserProfile, plan_nickname: str,
|
||||||
signed_seat_count: str, salt: str) -> Tuple[Plan, int]:
|
signed_seat_count: str, salt: str,
|
||||||
if plan_nickname not in [Plan.CLOUD_ANNUAL, Plan.CLOUD_MONTHLY]:
|
billing_modality: str) -> Tuple[Plan, int]:
|
||||||
|
provided_plans = {
|
||||||
|
'charge_automatically': [Plan.CLOUD_ANNUAL, Plan.CLOUD_MONTHLY],
|
||||||
|
'send_invoice': [Plan.CLOUD_ANNUAL],
|
||||||
|
}
|
||||||
|
if plan_nickname not in provided_plans[billing_modality]:
|
||||||
billing_logger.warning("Tampered plan during realm upgrade. user: %s, realm: %s (%s)."
|
billing_logger.warning("Tampered plan during realm upgrade. user: %s, realm: %s (%s)."
|
||||||
% (user.id, user.realm.id, user.realm.string_id))
|
% (user.id, user.realm.id, user.realm.string_id))
|
||||||
raise BillingError('tampered plan', BillingError.CONTACT_SUPPORT)
|
raise BillingError('tampered plan', BillingError.CONTACT_SUPPORT)
|
||||||
@@ -76,14 +81,19 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
plan, seat_count = unsign_and_check_upgrade_parameters(
|
plan, seat_count = unsign_and_check_upgrade_parameters(
|
||||||
user, request.POST['plan'], request.POST['signed_seat_count'], request.POST['salt'])
|
user, request.POST['plan'], request.POST['signed_seat_count'], request.POST['salt'],
|
||||||
|
request.POST['billing_modality'])
|
||||||
if request.POST['billing_modality'] == 'send_invoice':
|
if request.POST['billing_modality'] == 'send_invoice':
|
||||||
|
try:
|
||||||
|
invoiced_seat_count = int(request.POST['invoiced_seat_count'])
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
invoiced_seat_count = -1
|
||||||
min_required_seat_count = max(seat_count, MIN_INVOICED_SEAT_COUNT)
|
min_required_seat_count = max(seat_count, MIN_INVOICED_SEAT_COUNT)
|
||||||
if int(request.POST['invoiced_seat_count']) < min_required_seat_count:
|
if invoiced_seat_count < min_required_seat_count:
|
||||||
raise BillingError(
|
raise BillingError(
|
||||||
'lowball seat count',
|
'lowball seat count',
|
||||||
"You must invoice for at least %d users." % (min_required_seat_count,))
|
"You must invoice for at least %d users." % (min_required_seat_count,))
|
||||||
seat_count = int(request.POST['invoiced_seat_count'])
|
seat_count = invoiced_seat_count
|
||||||
process_initial_upgrade(user, plan, seat_count, request.POST.get('stripeToken', None))
|
process_initial_upgrade(user, plan, seat_count, request.POST.get('stripeToken', None))
|
||||||
except BillingError as e:
|
except BillingError as e:
|
||||||
error_message = e.message
|
error_message = e.message
|
||||||
@@ -103,6 +113,8 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
|||||||
'seat_count': seat_count,
|
'seat_count': seat_count,
|
||||||
'signed_seat_count': signed_seat_count,
|
'signed_seat_count': signed_seat_count,
|
||||||
'salt': salt,
|
'salt': salt,
|
||||||
|
'min_seat_count_for_invoice': max(seat_count, MIN_INVOICED_SEAT_COUNT),
|
||||||
|
'default_invoice_days_until_due': DEFAULT_INVOICE_DAYS_UNTIL_DUE,
|
||||||
'plan': "Zulip Standard",
|
'plan': "Zulip Standard",
|
||||||
'nickname_monthly': Plan.CLOUD_MONTHLY,
|
'nickname_monthly': Plan.CLOUD_MONTHLY,
|
||||||
'nickname_annual': Plan.CLOUD_ANNUAL,
|
'nickname_annual': Plan.CLOUD_ANNUAL,
|
||||||
@@ -140,6 +152,7 @@ def billing_home(request: HttpRequest) -> HttpResponse:
|
|||||||
if stripe_customer.account_balance < 0: # nocoverage
|
if stripe_customer.account_balance < 0: # nocoverage
|
||||||
context.update({'account_credits': '{:,.2f}'.format(-stripe_customer.account_balance / 100.)})
|
context.update({'account_credits': '{:,.2f}'.format(-stripe_customer.account_balance / 100.)})
|
||||||
|
|
||||||
|
billed_by_invoice = False
|
||||||
subscription = extract_current_subscription(stripe_customer)
|
subscription = extract_current_subscription(stripe_customer)
|
||||||
if subscription:
|
if subscription:
|
||||||
plan_name = PLAN_NAMES[Plan.objects.get(stripe_plan_id=subscription.plan.id).nickname]
|
plan_name = PLAN_NAMES[Plan.objects.get(stripe_plan_id=subscription.plan.id).nickname]
|
||||||
@@ -148,6 +161,8 @@ def billing_home(request: HttpRequest) -> HttpResponse:
|
|||||||
renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format(
|
renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format(
|
||||||
dt=timestamp_to_datetime(subscription.current_period_end))
|
dt=timestamp_to_datetime(subscription.current_period_end))
|
||||||
renewal_amount = upcoming_invoice_total(customer.stripe_customer_id)
|
renewal_amount = upcoming_invoice_total(customer.stripe_customer_id)
|
||||||
|
if subscription.billing == 'send_invoice':
|
||||||
|
billed_by_invoice = True
|
||||||
# Can only get here by subscribing and then downgrading. We don't support downgrading
|
# Can only get here by subscribing and then downgrading. We don't support downgrading
|
||||||
# yet, but keeping this code here since we will soon.
|
# yet, but keeping this code here since we will soon.
|
||||||
else: # nocoverage
|
else: # nocoverage
|
||||||
@@ -162,6 +177,7 @@ def billing_home(request: HttpRequest) -> HttpResponse:
|
|||||||
'renewal_date': renewal_date,
|
'renewal_date': renewal_date,
|
||||||
'renewal_amount': '{:,.2f}'.format(renewal_amount / 100.),
|
'renewal_amount': '{:,.2f}'.format(renewal_amount / 100.),
|
||||||
'payment_method': payment_method_string(stripe_customer),
|
'payment_method': payment_method_string(stripe_customer),
|
||||||
|
'billed_by_invoice': billed_by_invoice,
|
||||||
'publishable_key': STRIPE_PUBLISHABLE_KEY,
|
'publishable_key': STRIPE_PUBLISHABLE_KEY,
|
||||||
'stripe_email': stripe_customer.email,
|
'stripe_email': stripe_customer.email,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -152,6 +152,15 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-button {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#invoiced_seat_count {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
#error-message-box {
|
#error-message-box {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -40,10 +40,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="payment-method" data-email="{{stripe_email}}" data-csrf="{{csrf_token}}" data-key="{{publishable_key}}">
|
<div class="tab-pane" id="payment-method" data-email="{{stripe_email}}" data-csrf="{{csrf_token}}" data-key="{{publishable_key}}">
|
||||||
<div id="payment-section">
|
<div id="payment-section">
|
||||||
<p>Your current payment method is <strong>{{ payment_method }}</strong>.</p>
|
<p>Current payment method: <strong>{{ payment_method }}</strong></p>
|
||||||
|
{% if not billed_by_invoice %}
|
||||||
<button id="update-card-button" class="stripe-button-el">
|
<button id="update-card-button" class="stripe-button-el">
|
||||||
<span id="update-card-button-span">Update card</span>
|
<span id="update-card-button-span">Update card</span>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="loading-section">
|
<div id="loading-section">
|
||||||
<div class="updating-card-logo">
|
<div class="updating-card-logo">
|
||||||
|
|||||||
@@ -18,12 +18,20 @@
|
|||||||
|
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
<h1>{% trans %}Upgrade to {{ plan }}{% endtrans %}</h1>
|
||||||
{% if error_message %}
|
{% if error_message %}
|
||||||
<div class="alert alert-danger" id="error-message-box">
|
<div class="alert alert-danger" id="upgrade-error-message-box">
|
||||||
{{ error_message }}
|
{{ error_message }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h1>{% trans %}Upgrade to {{ plan }}{% endtrans %}</h1>
|
|
||||||
|
<ul class="nav nav-tabs" id="upgrade-tabs">
|
||||||
|
<li class="active"><a data-toggle="tab" href="#autopay">Pay automatically</a></li>
|
||||||
|
<li><a data-toggle="tab" href="#invoice">Pay by invoice</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane active" id="autopay">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{{ csrf_input }}
|
{{ csrf_input }}
|
||||||
<input type="hidden" name="seat_count" value="{{ seat_count }}">
|
<input type="hidden" name="seat_count" value="{{ seat_count }}">
|
||||||
@@ -55,8 +63,7 @@
|
|||||||
<p>
|
<p>
|
||||||
You’ll initially be charged
|
You’ll initially be charged
|
||||||
<b>$<span id="charged_amount">{{ cloud_annual_price * seat_count }}</span></b>
|
<b>$<span id="charged_amount">{{ cloud_annual_price * seat_count }}</span></b>
|
||||||
for <b>{{ seat_count }}</b> users. We’ll automatically charge you
|
for <b>{{ seat_count }}</b> users.
|
||||||
when new users are added, or give you credit when users are deactivated.
|
|
||||||
</p>
|
</p>
|
||||||
<script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
<script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
||||||
data-key="{{ publishable_key }}"
|
data-key="{{ publishable_key }}"
|
||||||
@@ -80,7 +87,45 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</form>
|
</form>
|
||||||
<p>We can also bill by invoice for annual contracts over $2,000. Contact <a href="mailto:support@zulipchat.com">support@zulipchat.com</a> to pay by invoice or for any other billing questions.</p>
|
</div>
|
||||||
|
<div class="tab-pane" id="invoice">
|
||||||
|
<form method="post">
|
||||||
|
{{ csrf_input }}
|
||||||
|
<input type="hidden" name="signed_seat_count" value="{{ signed_seat_count }}">
|
||||||
|
<input type="hidden" name="salt" value="{{ salt }}">
|
||||||
|
<input type="hidden" name="billing_modality" value="send_invoice">
|
||||||
|
<div class="payment-schedule">
|
||||||
|
<h3>{{ _("Payment schedule") }}</h3>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="plan" value="{{ nickname_annual }}" data-amount="{{ cloud_annual_price }}" checked />
|
||||||
|
<div class="box">
|
||||||
|
<div class="schedule-time annually">{{ _("Pay annually") }}</div>
|
||||||
|
<div class="schedule-amount">
|
||||||
|
${{ cloud_annual_price_per_month }}/user/month
|
||||||
|
<div class="schedule-amount-2">
|
||||||
|
(${{ cloud_annual_price }}/user/year)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<h4>Number of users</h4>
|
||||||
|
<input type="text" id="invoiced_seat_count" name="invoiced_seat_count" value=""/>
|
||||||
|
<p>
|
||||||
|
We'll send you an invoice by email. You
|
||||||
|
must invoice for at least {{ min_seat_count_for_invoice }} users.
|
||||||
|
</p>
|
||||||
|
<button type="submit" class="stripe-button-el invoice-button">Buy Standard</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="support-link">
|
||||||
|
<p>
|
||||||
|
We're happy to help!
|
||||||
|
Contact <a href="mailto:support@zulipchat.com">support@zulipchat.com</a>
|
||||||
|
for any billing-related questions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user