mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	billing: Add frontend for upgrading by invoice.
This commit is contained in:
		@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "account_balance": 0,
 | 
			
		||||
  "created": 1542524016,
 | 
			
		||||
  "created": 1543088274,
 | 
			
		||||
  "currency": null,
 | 
			
		||||
  "default_source": null,
 | 
			
		||||
  "delinquent": false,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "account_balance": 0,
 | 
			
		||||
  "created": 1542524016,
 | 
			
		||||
  "created": 1543088274,
 | 
			
		||||
  "currency": "usd",
 | 
			
		||||
  "default_source": null,
 | 
			
		||||
  "delinquent": false,
 | 
			
		||||
@@ -28,12 +28,12 @@
 | 
			
		||||
      {
 | 
			
		||||
        "application_fee_percent": null,
 | 
			
		||||
        "billing": "send_invoice",
 | 
			
		||||
        "billing_cycle_anchor": 1542524017,
 | 
			
		||||
        "billing_cycle_anchor": 1543088275,
 | 
			
		||||
        "cancel_at_period_end": false,
 | 
			
		||||
        "canceled_at": null,
 | 
			
		||||
        "created": 1542524017,
 | 
			
		||||
        "current_period_end": 1574060017,
 | 
			
		||||
        "current_period_start": 1542524017,
 | 
			
		||||
        "created": 1543088275,
 | 
			
		||||
        "current_period_end": 1574624275,
 | 
			
		||||
        "current_period_start": 1543088275,
 | 
			
		||||
        "customer": "cus_NORMALIZED0001",
 | 
			
		||||
        "days_until_due": 30,
 | 
			
		||||
        "default_source": null,
 | 
			
		||||
@@ -43,7 +43,7 @@
 | 
			
		||||
        "items": {
 | 
			
		||||
          "data": [
 | 
			
		||||
            {
 | 
			
		||||
              "created": 1542524017,
 | 
			
		||||
              "created": 1543088275,
 | 
			
		||||
              "id": "si_NORMALIZED0001",
 | 
			
		||||
              "metadata": {},
 | 
			
		||||
              "object": "subscription_item",
 | 
			
		||||
@@ -102,7 +102,7 @@
 | 
			
		||||
          "usage_type": "licensed"
 | 
			
		||||
        },
 | 
			
		||||
        "quantity": 123,
 | 
			
		||||
        "start": 1542524017,
 | 
			
		||||
        "start": 1543088275,
 | 
			
		||||
        "status": "active",
 | 
			
		||||
        "tax_percent": 0.0,
 | 
			
		||||
        "trial_end": null,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,52 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "account_balance": 0,
 | 
			
		||||
  "created": 1542524016,
 | 
			
		||||
  "created": 1543088274,
 | 
			
		||||
  "currency": "usd",
 | 
			
		||||
  "default_source": {
 | 
			
		||||
    "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"
 | 
			
		||||
  },
 | 
			
		||||
  "default_source": null,
 | 
			
		||||
  "delinquent": false,
 | 
			
		||||
  "description": "zulip (Zulip Dev)",
 | 
			
		||||
  "discount": null,
 | 
			
		||||
@@ -61,56 +17,10 @@
 | 
			
		||||
  "object": "customer",
 | 
			
		||||
  "shipping": null,
 | 
			
		||||
  "sources": {
 | 
			
		||||
    "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"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "data": [],
 | 
			
		||||
    "has_more": false,
 | 
			
		||||
    "object": "list",
 | 
			
		||||
    "total_count": 1,
 | 
			
		||||
    "total_count": 0,
 | 
			
		||||
    "url": "/v1/customers/cus_NORMALIZED0001/sources"
 | 
			
		||||
  },
 | 
			
		||||
  "subscriptions": {
 | 
			
		||||
@@ -118,12 +28,12 @@
 | 
			
		||||
      {
 | 
			
		||||
        "application_fee_percent": null,
 | 
			
		||||
        "billing": "send_invoice",
 | 
			
		||||
        "billing_cycle_anchor": 1542524017,
 | 
			
		||||
        "billing_cycle_anchor": 1543088275,
 | 
			
		||||
        "cancel_at_period_end": false,
 | 
			
		||||
        "canceled_at": null,
 | 
			
		||||
        "created": 1542524017,
 | 
			
		||||
        "current_period_end": 1574060017,
 | 
			
		||||
        "current_period_start": 1542524017,
 | 
			
		||||
        "created": 1543088275,
 | 
			
		||||
        "current_period_end": 1574624275,
 | 
			
		||||
        "current_period_start": 1543088275,
 | 
			
		||||
        "customer": "cus_NORMALIZED0001",
 | 
			
		||||
        "days_until_due": 30,
 | 
			
		||||
        "default_source": null,
 | 
			
		||||
@@ -133,7 +43,7 @@
 | 
			
		||||
        "items": {
 | 
			
		||||
          "data": [
 | 
			
		||||
            {
 | 
			
		||||
              "created": 1542524017,
 | 
			
		||||
              "created": 1543088275,
 | 
			
		||||
              "id": "si_NORMALIZED0001",
 | 
			
		||||
              "metadata": {},
 | 
			
		||||
              "object": "subscription_item",
 | 
			
		||||
@@ -192,7 +102,7 @@
 | 
			
		||||
          "usage_type": "licensed"
 | 
			
		||||
        },
 | 
			
		||||
        "quantity": 8,
 | 
			
		||||
        "start": 1542524018,
 | 
			
		||||
        "start": 1543088276,
 | 
			
		||||
        "status": "active",
 | 
			
		||||
        "tax_percent": 0.0,
 | 
			
		||||
        "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,
 | 
			
		||||
      "currency": "usd",
 | 
			
		||||
      "customer": "cus_NORMALIZED0001",
 | 
			
		||||
      "date": 1542524017,
 | 
			
		||||
      "date": 1543088275,
 | 
			
		||||
      "default_source": null,
 | 
			
		||||
      "description": "",
 | 
			
		||||
      "discount": null,
 | 
			
		||||
      "due_date": 1545116017,
 | 
			
		||||
      "due_date": 1545680275,
 | 
			
		||||
      "ending_balance": null,
 | 
			
		||||
      "finalized_at": null,
 | 
			
		||||
      "hosted_invoice_url": null,
 | 
			
		||||
      "id": "in_1DXkA1Gh0CmXqmnwNApgkOR1",
 | 
			
		||||
      "id": "in_1Da6wxGh0CmXqmnwfILsY1kf",
 | 
			
		||||
      "invoice_pdf": null,
 | 
			
		||||
      "lines": {
 | 
			
		||||
        "data": [
 | 
			
		||||
@@ -35,8 +35,8 @@
 | 
			
		||||
            "metadata": {},
 | 
			
		||||
            "object": "line_item",
 | 
			
		||||
            "period": {
 | 
			
		||||
              "end": 1574060017,
 | 
			
		||||
              "start": 1542524017
 | 
			
		||||
              "end": 1574624275,
 | 
			
		||||
              "start": 1543088275
 | 
			
		||||
            },
 | 
			
		||||
            "plan": {
 | 
			
		||||
              "active": true,
 | 
			
		||||
@@ -69,7 +69,7 @@
 | 
			
		||||
        "has_more": false,
 | 
			
		||||
        "object": "list",
 | 
			
		||||
        "total_count": 1,
 | 
			
		||||
        "url": "/v1/invoices/in_1DXkA1Gh0CmXqmnwNApgkOR1/lines"
 | 
			
		||||
        "url": "/v1/invoices/in_1Da6wxGh0CmXqmnwfILsY1kf/lines"
 | 
			
		||||
      },
 | 
			
		||||
      "livemode": false,
 | 
			
		||||
      "metadata": {},
 | 
			
		||||
@@ -78,8 +78,8 @@
 | 
			
		||||
      "object": "invoice",
 | 
			
		||||
      "paid": false,
 | 
			
		||||
      "payment_intent": null,
 | 
			
		||||
      "period_end": 1542524017,
 | 
			
		||||
      "period_start": 1542524017,
 | 
			
		||||
      "period_end": 1543088275,
 | 
			
		||||
      "period_start": 1543088275,
 | 
			
		||||
      "receipt_number": null,
 | 
			
		||||
      "starting_balance": 0,
 | 
			
		||||
      "statement_descriptor": null,
 | 
			
		||||
@@ -89,7 +89,7 @@
 | 
			
		||||
      "tax": 0,
 | 
			
		||||
      "tax_percent": 0.0,
 | 
			
		||||
      "total": 984000,
 | 
			
		||||
      "webhooks_delivered_at": 1542524017
 | 
			
		||||
      "webhooks_delivered_at": 1543088275
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "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,
 | 
			
		||||
  "billing": "send_invoice",
 | 
			
		||||
  "billing_cycle_anchor": 1542524017,
 | 
			
		||||
  "billing_cycle_anchor": 1543088275,
 | 
			
		||||
  "cancel_at_period_end": false,
 | 
			
		||||
  "canceled_at": null,
 | 
			
		||||
  "created": 1542524017,
 | 
			
		||||
  "current_period_end": 1574060017,
 | 
			
		||||
  "current_period_start": 1542524017,
 | 
			
		||||
  "created": 1543088275,
 | 
			
		||||
  "current_period_end": 1574624275,
 | 
			
		||||
  "current_period_start": 1543088275,
 | 
			
		||||
  "customer": "cus_NORMALIZED0001",
 | 
			
		||||
  "days_until_due": 30,
 | 
			
		||||
  "default_source": null,
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
  "items": {
 | 
			
		||||
    "data": [
 | 
			
		||||
      {
 | 
			
		||||
        "created": 1542524017,
 | 
			
		||||
        "created": 1543088275,
 | 
			
		||||
        "id": "si_NORMALIZED0001",
 | 
			
		||||
        "metadata": {},
 | 
			
		||||
        "object": "subscription_item",
 | 
			
		||||
@@ -75,7 +75,7 @@
 | 
			
		||||
    "usage_type": "licensed"
 | 
			
		||||
  },
 | 
			
		||||
  "quantity": 123,
 | 
			
		||||
  "start": 1542524017,
 | 
			
		||||
  "start": 1543088275,
 | 
			
		||||
  "status": "active",
 | 
			
		||||
  "tax_percent": 0.0,
 | 
			
		||||
  "trial_end": null,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "application_fee_percent": null,
 | 
			
		||||
  "billing": "send_invoice",
 | 
			
		||||
  "billing_cycle_anchor": 1542524017,
 | 
			
		||||
  "billing_cycle_anchor": 1543088275,
 | 
			
		||||
  "cancel_at_period_end": false,
 | 
			
		||||
  "canceled_at": null,
 | 
			
		||||
  "created": 1542524017,
 | 
			
		||||
  "current_period_end": 1574060017,
 | 
			
		||||
  "current_period_start": 1542524017,
 | 
			
		||||
  "created": 1543088275,
 | 
			
		||||
  "current_period_end": 1574624275,
 | 
			
		||||
  "current_period_start": 1543088275,
 | 
			
		||||
  "customer": "cus_NORMALIZED0001",
 | 
			
		||||
  "days_until_due": 30,
 | 
			
		||||
  "default_source": null,
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
  "items": {
 | 
			
		||||
    "data": [
 | 
			
		||||
      {
 | 
			
		||||
        "created": 1542524017,
 | 
			
		||||
        "created": 1543088275,
 | 
			
		||||
        "id": "si_NORMALIZED0001",
 | 
			
		||||
        "metadata": {},
 | 
			
		||||
        "object": "subscription_item",
 | 
			
		||||
@@ -75,7 +75,7 @@
 | 
			
		||||
    "usage_type": "licensed"
 | 
			
		||||
  },
 | 
			
		||||
  "quantity": 8,
 | 
			
		||||
  "start": 1542524018,
 | 
			
		||||
  "start": 1543088276,
 | 
			
		||||
  "status": "active",
 | 
			
		||||
  "tax_percent": 0.0,
 | 
			
		||||
  "trial_end": null,
 | 
			
		||||
 
 | 
			
		||||
@@ -276,7 +276,7 @@ class StripeTest(ZulipTestCase):
 | 
			
		||||
        user = self.example_user("hamlet")
 | 
			
		||||
        self.login(user.email)
 | 
			
		||||
        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.assertNotEqual(user.realm.plan_type, Realm.STANDARD)
 | 
			
		||||
        self.assertFalse(Customer.objects.filter(realm=user.realm).exists())
 | 
			
		||||
@@ -331,9 +331,9 @@ class StripeTest(ZulipTestCase):
 | 
			
		||||
 | 
			
		||||
        # Check /billing has the correct information
 | 
			
		||||
        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,),
 | 
			
		||||
                          'Card ending in 4242']:
 | 
			
		||||
                          'Card ending in 4242', 'Update card']:
 | 
			
		||||
            self.assert_in_response(substring, response)
 | 
			
		||||
 | 
			
		||||
    @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')
 | 
			
		||||
 | 
			
		||||
    def test_upgrade_with_tampered_plan(self) -> None:
 | 
			
		||||
        # Test with an unknown plan
 | 
			
		||||
        self.login(self.example_email("hamlet"))
 | 
			
		||||
        response = self.client_post("/upgrade/", {
 | 
			
		||||
            'stripeToken': self.token,
 | 
			
		||||
@@ -484,6 +485,16 @@ class StripeTest(ZulipTestCase):
 | 
			
		||||
        })
 | 
			
		||||
        self.assert_in_success_response(["Upgrade to Zulip Standard"], response)
 | 
			
		||||
        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:
 | 
			
		||||
        self.login(self.example_email("hamlet"))
 | 
			
		||||
@@ -510,6 +521,16 @@ class StripeTest(ZulipTestCase):
 | 
			
		||||
        self.assert_in_success_response(["Upgrade to Zulip Standard",
 | 
			
		||||
                                         "at least %d users" % (self.quantity,)], response)
 | 
			
		||||
        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")
 | 
			
		||||
    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')
 | 
			
		||||
 | 
			
		||||
    @mock_stripe("Customer.create", "Subscription.create", "Subscription.save",
 | 
			
		||||
                 "Customer.retrieve", "Invoice.list")
 | 
			
		||||
    def test_upgrade_billing_by_invoice(self, mock5: Mock, mock4: Mock, mock3: Mock,
 | 
			
		||||
                 "Customer.retrieve", "Invoice.list", "Invoice.upcoming")
 | 
			
		||||
    def test_upgrade_billing_by_invoice(self, mock6: Mock, mock5: Mock, mock4: Mock, mock3: Mock,
 | 
			
		||||
                                        mock2: Mock, mock1: Mock) -> None:
 | 
			
		||||
        user = self.example_user("hamlet")
 | 
			
		||||
        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()),
 | 
			
		||||
            {'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)
 | 
			
		||||
    def test_redirect_for_billing_home(self, mock_customer_with_subscription: Mock) -> None:
 | 
			
		||||
        user = self.example_user("iago")
 | 
			
		||||
@@ -612,7 +639,7 @@ class StripeTest(ZulipTestCase):
 | 
			
		||||
 | 
			
		||||
        with patch("corporate.views.upcoming_invoice_total", return_value=0):
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
    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, \
 | 
			
		||||
    extract_current_subscription, process_initial_upgrade, sign_string, \
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
billing_logger = logging.getLogger('corporate.stripe')
 | 
			
		||||
 | 
			
		||||
def unsign_and_check_upgrade_parameters(user: UserProfile, plan_nickname: str,
 | 
			
		||||
                                        signed_seat_count: str, salt: str) -> Tuple[Plan, int]:
 | 
			
		||||
    if plan_nickname not in [Plan.CLOUD_ANNUAL, Plan.CLOUD_MONTHLY]:
 | 
			
		||||
                                        signed_seat_count: str, salt: str,
 | 
			
		||||
                                        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)."
 | 
			
		||||
                               % (user.id, user.realm.id, user.realm.string_id))
 | 
			
		||||
        raise BillingError('tampered plan', BillingError.CONTACT_SUPPORT)
 | 
			
		||||
@@ -76,14 +81,19 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
 | 
			
		||||
    if request.method == 'POST':
 | 
			
		||||
        try:
 | 
			
		||||
            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':
 | 
			
		||||
                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)
 | 
			
		||||
                if int(request.POST['invoiced_seat_count']) < min_required_seat_count:
 | 
			
		||||
                if invoiced_seat_count < min_required_seat_count:
 | 
			
		||||
                    raise BillingError(
 | 
			
		||||
                        'lowball 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))
 | 
			
		||||
        except BillingError as e:
 | 
			
		||||
            error_message = e.message
 | 
			
		||||
@@ -103,6 +113,8 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
 | 
			
		||||
        'seat_count': seat_count,
 | 
			
		||||
        'signed_seat_count': signed_seat_count,
 | 
			
		||||
        '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",
 | 
			
		||||
        'nickname_monthly': Plan.CLOUD_MONTHLY,
 | 
			
		||||
        'nickname_annual': Plan.CLOUD_ANNUAL,
 | 
			
		||||
@@ -140,6 +152,7 @@ def billing_home(request: HttpRequest) -> HttpResponse:
 | 
			
		||||
    if stripe_customer.account_balance < 0:  # nocoverage
 | 
			
		||||
        context.update({'account_credits': '{:,.2f}'.format(-stripe_customer.account_balance / 100.)})
 | 
			
		||||
 | 
			
		||||
    billed_by_invoice = False
 | 
			
		||||
    subscription = extract_current_subscription(stripe_customer)
 | 
			
		||||
    if subscription:
 | 
			
		||||
        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(
 | 
			
		||||
            dt=timestamp_to_datetime(subscription.current_period_end))
 | 
			
		||||
        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
 | 
			
		||||
    # yet, but keeping this code here since we will soon.
 | 
			
		||||
    else:  # nocoverage
 | 
			
		||||
@@ -162,6 +177,7 @@ def billing_home(request: HttpRequest) -> HttpResponse:
 | 
			
		||||
        'renewal_date': renewal_date,
 | 
			
		||||
        'renewal_amount': '{:,.2f}'.format(renewal_amount / 100.),
 | 
			
		||||
        'payment_method': payment_method_string(stripe_customer),
 | 
			
		||||
        'billed_by_invoice': billed_by_invoice,
 | 
			
		||||
        'publishable_key': STRIPE_PUBLISHABLE_KEY,
 | 
			
		||||
        'stripe_email': stripe_customer.email,
 | 
			
		||||
    })
 | 
			
		||||
 
 | 
			
		||||
@@ -152,6 +152,15 @@
 | 
			
		||||
        pointer-events: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .invoice-button {
 | 
			
		||||
        font-size: 17px;
 | 
			
		||||
        font-weight: 700 !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #invoiced_seat_count {
 | 
			
		||||
        width: 50px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #error-message-box {
 | 
			
		||||
        margin-top: 10px;
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
 
 | 
			
		||||
@@ -40,10 +40,12 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="tab-pane" id="payment-method" data-email="{{stripe_email}}" data-csrf="{{csrf_token}}" data-key="{{publishable_key}}">
 | 
			
		||||
                        <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">
 | 
			
		||||
                                <span id="update-card-button-span">Update card</span>
 | 
			
		||||
                            </button>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div id="loading-section">
 | 
			
		||||
                            <div class="updating-card-logo">
 | 
			
		||||
 
 | 
			
		||||
@@ -18,12 +18,20 @@
 | 
			
		||||
 | 
			
		||||
        <div class="page-content">
 | 
			
		||||
            <div class="main">
 | 
			
		||||
                <h1>{% trans %}Upgrade to {{ plan }}{% endtrans %}</h1>
 | 
			
		||||
                {% if error_message %}
 | 
			
		||||
                <div class="alert alert-danger" id="error-message-box">
 | 
			
		||||
                <div class="alert alert-danger" id="upgrade-error-message-box">
 | 
			
		||||
                    {{ error_message }}
 | 
			
		||||
                </div>
 | 
			
		||||
                {% 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">
 | 
			
		||||
                            {{ csrf_input }}
 | 
			
		||||
                            <input type="hidden" name="seat_count" value="{{ seat_count }}">
 | 
			
		||||
@@ -55,8 +63,7 @@
 | 
			
		||||
                            <p>
 | 
			
		||||
                                You’ll initially be charged
 | 
			
		||||
                                <b>$<span id="charged_amount">{{ cloud_annual_price * seat_count }}</span></b>
 | 
			
		||||
                        for <b>{{ seat_count }}</b> users. We’ll automatically charge you
 | 
			
		||||
                        when new users are added, or give you credit when users are deactivated.
 | 
			
		||||
                                for <b>{{ seat_count }}</b> users.
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
 | 
			
		||||
                                    data-key="{{ publishable_key }}"
 | 
			
		||||
@@ -80,7 +87,45 @@
 | 
			
		||||
                                });
 | 
			
		||||
                            </script>
 | 
			
		||||
                        </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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user