mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-30 19:43:47 +00:00 
			
		
		
		
	Compare commits
	
		
			53 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 8e7ac21fe0 | ||
|  | cbfae3e0d0 | ||
|  | ffeb4340a9 | ||
|  | 79f781b9ea | ||
|  | fd89df63b4 | ||
|  | 509d335705 | ||
|  | ce28ccf2bf | ||
|  | 4adbeedef6 | ||
|  | 09ed7d5b77 | ||
|  | f445d3f589 | ||
|  | e8ee374d4f | ||
|  | 9ff5359522 | ||
|  | a31f56443a | ||
|  | 0b263d8b8c | ||
|  | ad00b02c66 | ||
|  | 02f2ae4048 | ||
|  | 56d4426738 | ||
|  | f0fe7d3887 | ||
|  | 21166fbdf9 | ||
|  | b2c865aab5 | ||
|  | fd9847ffcb | ||
|  | 2bca1d4ef0 | ||
|  | 871fdddf86 | ||
|  | bf4e01eb02 | ||
|  | 92ea9a0729 | ||
|  | a36cb9b247 | ||
|  | da71f67f85 | ||
|  | 5518abe1d6 | ||
|  | b34848447e | ||
|  | 3d5c994a32 | ||
|  | cfa2c6bf37 | ||
|  | 3d243a457f | ||
|  | e414036eb3 | ||
|  | 9ec154bbe8 | ||
|  | da479c3613 | ||
|  | 23d8f6e6b0 | ||
|  | d929ad0122 | ||
|  | 429d0f9728 | ||
|  | c905915055 | ||
|  | b238d0e75e | ||
|  | a4ebdac521 | ||
|  | bdd6e1fabe | ||
|  | d120ee25b4 | ||
|  | 14938fbf4c | ||
|  | 8babe17f8f | ||
|  | 724e1d3002 | ||
|  | a474a08195 | ||
|  | a48bbef766 | ||
|  | 71c5632e07 | ||
|  | 498b6e4670 | ||
|  | 925ddc28f4 | ||
|  | 3651b8d254 | ||
|  | 90d3cbceed | 
| @@ -1,86 +1,7 @@ | ||||
| # See https://zulip.readthedocs.io/en/latest/testing/continuous-integration.html for | ||||
| #   high-level documentation on our CircleCI setup. | ||||
| # See CircleCI upstream's docs on this config format: | ||||
| #   https://circleci.com/docs/2.0/language-python/ | ||||
| # | ||||
| version: 2 | ||||
| aliases: | ||||
|   - &create_cache_directories | ||||
|     run: | ||||
|       name: create cache directories | ||||
|       command: | | ||||
|           dirs=(/srv/zulip-{npm,venv}-cache) | ||||
|           sudo mkdir -p "${dirs[@]}" | ||||
|           sudo chown -R circleci "${dirs[@]}" | ||||
|  | ||||
|   - &restore_cache_package_json | ||||
|     restore_cache: | ||||
|       keys: | ||||
|       - v1-npm-base.{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} | ||||
|  | ||||
|   - &restore_cache_requirements | ||||
|     restore_cache: | ||||
|       keys: | ||||
|       - v1-venv-base.{{ .Environment.CIRCLE_JOB }}-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }} | ||||
|  | ||||
|   - &install_dependencies | ||||
|     run: | ||||
|       name: install dependencies | ||||
|       command: | | ||||
|         sudo apt-get update | ||||
|         # Install moreutils so we can use `ts` and `mispipe` in the following. | ||||
|         sudo apt-get install -y moreutils | ||||
|  | ||||
|         # CircleCI sets the following in Git config at clone time: | ||||
|         #   url.ssh://git@github.com.insteadOf https://github.com | ||||
|         # This breaks the Git clones in the NVM `install.sh` we run | ||||
|         # in `install-node`. | ||||
|         # TODO: figure out why that breaks, and whether we want it. | ||||
|         #   (Is it an optimization?) | ||||
|         rm -f /home/circleci/.gitconfig | ||||
|  | ||||
|         # This is the main setup job for the test suite | ||||
|         mispipe "tools/ci/setup-backend" ts | ||||
|  | ||||
|         # Cleaning caches is mostly unnecessary in Circle, because | ||||
|         # most builds don't get to write to the cache. | ||||
|         # mispipe "scripts/lib/clean-unused-caches --verbose --threshold 0" ts | ||||
|  | ||||
|   - &save_cache_package_json | ||||
|     save_cache: | ||||
|       paths: | ||||
|         - /srv/zulip-npm-cache | ||||
|       key: v1-npm-base.{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} | ||||
|  | ||||
|   - &save_cache_requirements | ||||
|     save_cache: | ||||
|       paths: | ||||
|         - /srv/zulip-venv-cache | ||||
|       key: v1-venv-base.{{ .Environment.CIRCLE_JOB }}-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }} | ||||
|     # TODO: in Travis we also cache ~/zulip-emoji-cache, ~/node, ~/misc | ||||
|  | ||||
|   - &run_backend_tests | ||||
|     run: | ||||
|       name: run backend tests | ||||
|       command: | | ||||
|         . /srv/zulip-py3-venv/bin/activate | ||||
|         mispipe ./tools/ci/backend ts | ||||
|  | ||||
|   - &run_frontend_tests | ||||
|     run: | ||||
|       name: run frontend tests | ||||
|       command: | | ||||
|         . /srv/zulip-py3-venv/bin/activate | ||||
|         mispipe ./tools/ci/frontend ts | ||||
|  | ||||
|   - &upload_coverage_report | ||||
|     run: | ||||
|      name: upload coverage report | ||||
|      command: | | ||||
|        . /srv/zulip-py3-venv/bin/activate | ||||
|        pip install codecov && codecov \ | ||||
|          || echo "Error in uploading coverage reports to codecov.io." | ||||
|  | ||||
| jobs: | ||||
|   "trusty-python-3.4": | ||||
|     docker: | ||||
| @@ -92,15 +13,71 @@ jobs: | ||||
|     steps: | ||||
|       - checkout | ||||
|  | ||||
|       - *create_cache_directories | ||||
|       - *restore_cache_package_json | ||||
|       - *restore_cache_requirements | ||||
|       - *install_dependencies | ||||
|       - *save_cache_package_json | ||||
|       - *save_cache_requirements | ||||
|       - *run_backend_tests | ||||
|       - *run_frontend_tests | ||||
|       - *upload_coverage_report | ||||
|       - run: | ||||
|           name: create cache directories | ||||
|           command: | | ||||
|               dirs=(/srv/zulip-{npm,venv}-cache) | ||||
|               sudo mkdir -p "${dirs[@]}" | ||||
|               sudo chown -R circleci "${dirs[@]}" | ||||
|  | ||||
|       - restore_cache: | ||||
|           keys: | ||||
|           - v1-npm-base.trusty-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} | ||||
|       - restore_cache: | ||||
|           keys: | ||||
|           - v1-venv-base.trusty-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }} | ||||
|  | ||||
|       - run: | ||||
|           name: install dependencies | ||||
|           command: | | ||||
|             # Install moreutils so we can use `ts` and `mispipe` in the following. | ||||
|             sudo apt-get install -y moreutils | ||||
|  | ||||
|             # CircleCI sets the following in Git config at clone time: | ||||
|             #   url.ssh://git@github.com.insteadOf https://github.com | ||||
|             # This breaks the Git clones in the NVM `install.sh` we run | ||||
|             # in `install-node`. | ||||
|             # TODO: figure out why that breaks, and whether we want it. | ||||
|             #   (Is it an optimization?) | ||||
|             rm -f /home/circleci/.gitconfig | ||||
|  | ||||
|             # This is the main setup job for the test suite | ||||
|             mispipe "tools/travis/setup-backend" ts | ||||
|  | ||||
|             # Cleaning caches is mostly unnecessary in Circle, because | ||||
|             # most builds don't get to write to the cache. | ||||
|             # mispipe "scripts/lib/clean-unused-caches --verbose --threshold 0" ts | ||||
|  | ||||
|       - save_cache: | ||||
|           paths: | ||||
|             - /srv/zulip-npm-cache | ||||
|           key: v1-npm-base.trusty-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} | ||||
|       - save_cache: | ||||
|           paths: | ||||
|             - /srv/zulip-venv-cache | ||||
|           key: v1-venv-base.trusty-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }} | ||||
|       # TODO: in Travis we also cache ~/zulip-emoji-cache, ~/node, ~/misc | ||||
|  | ||||
|       # The moment of truth!  Run the tests. | ||||
|  | ||||
|       - run: | ||||
|           name: run backend tests | ||||
|           command: | | ||||
|             . /srv/zulip-py3-venv/bin/activate | ||||
|             mispipe ./tools/travis/backend ts | ||||
|  | ||||
|       - run: | ||||
|           name: run frontend tests | ||||
|           command: | | ||||
|             . /srv/zulip-py3-venv/bin/activate | ||||
|             mispipe ./tools/travis/frontend ts | ||||
|  | ||||
|       -  run: | ||||
|           name: upload coverage report | ||||
|           command: | | ||||
|             . /srv/zulip-py3-venv/bin/activate | ||||
|             pip install codecov && codecov \ | ||||
|               || echo "Error in uploading coverage reports to codecov.io." | ||||
|  | ||||
|       # - store_artifacts:  # TODO | ||||
|       #     path: var/casper/ | ||||
| @@ -116,14 +93,50 @@ jobs: | ||||
|  | ||||
|     steps: | ||||
|       - checkout | ||||
|       - *create_cache_directories | ||||
|       - *restore_cache_package_json | ||||
|       - *restore_cache_requirements | ||||
|       - *install_dependencies | ||||
|       - *save_cache_package_json | ||||
|       - *save_cache_requirements | ||||
|       - *run_backend_tests | ||||
|       - *upload_coverage_report | ||||
|  | ||||
|       - run: | ||||
|           name: create cache directories | ||||
|           command: | | ||||
|               dirs=(/srv/zulip-{npm,venv}-cache) | ||||
|               sudo mkdir -p "${dirs[@]}" | ||||
|               sudo chown -R circleci "${dirs[@]}" | ||||
|  | ||||
|       - restore_cache: | ||||
|           keys: | ||||
|           - v1-npm-base.xenial-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} | ||||
|       - restore_cache: | ||||
|           keys: | ||||
|           - v1-venv-base.xenial-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }} | ||||
|  | ||||
|       - run: | ||||
|           name: install dependencies | ||||
|           command: | | ||||
|             sudo apt-get update | ||||
|             sudo apt-get install -y moreutils | ||||
|             rm -f /home/circleci/.gitconfig | ||||
|             mispipe "tools/travis/setup-backend" ts | ||||
|  | ||||
|       - save_cache: | ||||
|           paths: | ||||
|             - /srv/zulip-npm-cache | ||||
|           key: v1-npm-base.xenial-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} | ||||
|       - save_cache: | ||||
|           paths: | ||||
|             - /srv/zulip-venv-cache | ||||
|           key: v1-venv-base.xenial-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }} | ||||
|  | ||||
|       - run: | ||||
|           name: run backend tests | ||||
|           command: | | ||||
|             . /srv/zulip-py3-venv/bin/activate | ||||
|             mispipe ./tools/travis/backend ts | ||||
|  | ||||
|       -  run: | ||||
|           name: upload coverage report | ||||
|           command: | | ||||
|             . /srv/zulip-py3-venv/bin/activate | ||||
|             pip install codecov && codecov \ | ||||
|               || echo "Error in uploading coverage reports to codecov.io." | ||||
|  | ||||
|   "bionic-python-3.6": | ||||
|     docker: | ||||
| @@ -135,22 +148,53 @@ jobs: | ||||
|     steps: | ||||
|       - checkout | ||||
|  | ||||
|       - *create_cache_directories | ||||
|  | ||||
|       - run: | ||||
|           name: do Bionic hack | ||||
|           name: create cache directories | ||||
|           command: | | ||||
|               dirs=(/srv/zulip-{npm,venv}-cache) | ||||
|               sudo mkdir -p "${dirs[@]}" | ||||
|               sudo chown -R circleci "${dirs[@]}" | ||||
|               # Temporary hack till `sudo service redis-server start` gets fixes in Bionic. See | ||||
|               # https://chat.zulip.org/#narrow/stream/3-backend/topic/Ubuntu.20bionic.20CircleCI | ||||
|               redis-server --daemonize yes | ||||
|  | ||||
|       - *restore_cache_package_json | ||||
|       - *restore_cache_requirements | ||||
|       - *install_dependencies | ||||
|       - *save_cache_package_json | ||||
|       - *save_cache_requirements | ||||
|       - *run_backend_tests | ||||
|       - *upload_coverage_report | ||||
|       - restore_cache: | ||||
|           keys: | ||||
|           - v1-npm-base.bionic-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} | ||||
|       - restore_cache: | ||||
|           keys: | ||||
|           - v1-venv-base.bionic-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }} | ||||
|  | ||||
|       - run: | ||||
|           name: install dependencies | ||||
|           command: | | ||||
|             sudo apt-get update | ||||
|             sudo apt-get install -y moreutils | ||||
|             rm -f /home/circleci/.gitconfig | ||||
|             mispipe "tools/travis/setup-backend" ts | ||||
|  | ||||
|       - save_cache: | ||||
|           paths: | ||||
|             - /srv/zulip-npm-cache | ||||
|           key: v1-npm-base.bionic-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} | ||||
|       - save_cache: | ||||
|           paths: | ||||
|             - /srv/zulip-venv-cache | ||||
|           key: v1-venv-base.bionic-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }} | ||||
|  | ||||
|       - run: | ||||
|           name: run backend tests | ||||
|           command: | | ||||
|             . /srv/zulip-py3-venv/bin/activate | ||||
|             mispipe ./tools/travis/backend ts | ||||
|  | ||||
|       -  run: | ||||
|           name: upload coverage report | ||||
|           command: | | ||||
|             . /srv/zulip-py3-venv/bin/activate | ||||
|             pip install codecov && codecov \ | ||||
|               || echo "Error in uploading coverage reports to codecov.io." | ||||
|  | ||||
|  | ||||
| workflows: | ||||
|   version: 2 | ||||
|   | ||||
| @@ -26,11 +26,11 @@ | ||||
|         "_": false, | ||||
|         "activity": false, | ||||
|         "admin": false, | ||||
|         "admin_sections": false, | ||||
|         "alert_words": false, | ||||
|         "alert_words_ui": false, | ||||
|         "attachments_ui": false, | ||||
|         "avatar": false, | ||||
|         "billing": false, | ||||
|         "blueslip": false, | ||||
|         "bot_data": false, | ||||
|         "bridge": false, | ||||
| @@ -38,7 +38,6 @@ | ||||
|         "buddy_list": false, | ||||
|         "channel": false, | ||||
|         "click_handlers": false, | ||||
|         "color_data": false, | ||||
|         "colorspace": false, | ||||
|         "common": false, | ||||
|         "components": false, | ||||
| @@ -61,14 +60,12 @@ | ||||
|         "emoji_picker": false, | ||||
|         "favicon": false, | ||||
|         "feature_flags": false, | ||||
|         "feedback_widget": false, | ||||
|         "fenced_code": false, | ||||
|         "flatpickr": false, | ||||
|         "floating_recipient_bar": false, | ||||
|         "gear_menu": false, | ||||
|         "hash_util": false, | ||||
|         "hashchange": false, | ||||
|         "helpers": false, | ||||
|         "home_msg_list": false, | ||||
|         "hotspots": false, | ||||
|         "i18n": false, | ||||
| @@ -120,7 +117,6 @@ | ||||
|         "pygments_data": false, | ||||
|         "reactions": false, | ||||
|         "realm_icon": false, | ||||
|         "realm_logo": false, | ||||
|         "recent_senders": false, | ||||
|         "reload": false, | ||||
|         "reload_state": false, | ||||
| @@ -145,7 +141,7 @@ | ||||
|         "settings_bots": false, | ||||
|         "settings_display": false, | ||||
|         "settings_emoji": false, | ||||
|         "settings_linkifiers": false, | ||||
|         "settings_filters": false, | ||||
|         "settings_invites": false, | ||||
|         "settings_muting": false, | ||||
|         "settings_notifications": false, | ||||
| @@ -168,7 +164,6 @@ | ||||
|         "stream_muting": false, | ||||
|         "stream_popover": false, | ||||
|         "stream_sort": false, | ||||
|         "StripeCheckout": false, | ||||
|         "submessage": false, | ||||
|         "subs": false, | ||||
|         "tab_bar": false, | ||||
| @@ -190,23 +185,19 @@ | ||||
|         "typing_events": false, | ||||
|         "typing_status": false, | ||||
|         "ui": false, | ||||
|         "ui_init": false, | ||||
|         "ui_report": false, | ||||
|         "ui_util": false, | ||||
|         "unread": false, | ||||
|         "unread_ops": false, | ||||
|         "unread_ui": false, | ||||
|         "upgrade": false, | ||||
|         "upload": false, | ||||
|         "upload_widget": false, | ||||
|         "user_events": false, | ||||
|         "user_groups": false, | ||||
|         "user_pill": false, | ||||
|         "user_search": false, | ||||
|         "user_status": false, | ||||
|         "user_status_ui": false, | ||||
|         "util": false, | ||||
|         "poll_widget": false, | ||||
|         "voting_widget": false, | ||||
|         "widgetize": false, | ||||
|         "zcommand": false, | ||||
|         "zform": false, | ||||
| @@ -231,12 +222,6 @@ | ||||
|                 "functions": "never" | ||||
|             } | ||||
|         ], | ||||
|         "comma-spacing": [ "error", | ||||
|             { | ||||
|                 "before": false, | ||||
|                 "after": true | ||||
|             } | ||||
|         ], | ||||
|         "complexity": [ 0, 4 ], | ||||
|         "curly": 2, | ||||
|         "dot-notation": [ "error", { "allowKeywords": true } ], | ||||
| @@ -254,12 +239,6 @@ | ||||
|             "FunctionExpression": {"parameters": "first"}, | ||||
|             "FunctionDeclaration": {"parameters": "first"} | ||||
|         }], | ||||
|         "key-spacing": [ "error", | ||||
|             { | ||||
|                 "beforeColon": false, | ||||
|                 "afterColon": true | ||||
|             } | ||||
|         ], | ||||
|         "keyword-spacing": [ "error", | ||||
|             { | ||||
|                 "before": true, | ||||
| @@ -386,7 +365,6 @@ | ||||
|         "quotes": [ 0, "single" ], | ||||
|         "radix": 2, | ||||
|         "semi": 2, | ||||
|         "semi-spacing": [2, {"before": false, "after": true}], | ||||
|         "space-before-blocks": 2, | ||||
|         "space-before-function-paren": [ "error", | ||||
|             { | ||||
|   | ||||
| @@ -62,6 +62,5 @@ | ||||
|          | ||||
|         # Limit language features | ||||
|         "color-no-hex": true, | ||||
|         "color-named": "never", | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # See https://zulip.readthedocs.io/en/latest/testing/continuous-integration.html for | ||||
| # See https://zulip.readthedocs.io/en/latest/testing/travis.html for | ||||
| # high-level documentation on our Travis CI setup. | ||||
| dist: trusty | ||||
| group: deprecated-2017Q4 | ||||
| @@ -15,7 +15,7 @@ install: | ||||
|   - mispipe "pip install codecov" ts || mispipe "pip install codecov" ts | ||||
|  | ||||
|   # This is the main setup job for the test suite | ||||
|   - mispipe "tools/ci/setup-$TEST_SUITE" ts | ||||
|   - mispipe "tools/travis/setup-$TEST_SUITE" ts | ||||
|  | ||||
|   # Clean any caches that are not in use to avoid our cache | ||||
|   # becoming huge. | ||||
| @@ -26,7 +26,7 @@ script: | ||||
|   # broken running their system puppet with Ruby.  See | ||||
|   # https://travis-ci.org/zulip/zulip/jobs/240120991 for an example traceback. | ||||
|   - unset GEM_PATH | ||||
|   - mispipe "./tools/ci/$TEST_SUITE" ts | ||||
|   - mispipe "./tools/travis/$TEST_SUITE" ts | ||||
| cache: | ||||
|   yarn: true | ||||
|   apt: false | ||||
| @@ -38,7 +38,7 @@ cache: | ||||
|     - $HOME/misc | ||||
| env: | ||||
|   global: | ||||
|     - BOTO_CONFIG=/nonexistent | ||||
|     - BOTO_CONFIG=/tmp/nowhere | ||||
| language: python | ||||
| # Our test suites generally run on Python 3.4, the version in | ||||
| # Ubuntu 14.04 trusty, which is the oldest OS release we support. | ||||
|   | ||||
| @@ -69,11 +69,10 @@ to help. | ||||
|   if you run into any troubles. | ||||
| * Read the | ||||
|   [Zulip guide to Git](https://zulip.readthedocs.io/en/latest/git/index.html) | ||||
|   and do the Git tutorial (coming soon) if you are unfamiliar with | ||||
|   Git, getting help in | ||||
|   [#git help](https://chat.zulip.org/#narrow/stream/44-git-help) if | ||||
|   you run into any troubles.  Be sure to check out the | ||||
|   [extremely useful Zulip-specific tools page](https://zulip.readthedocs.io/en/latest/git/zulip-tools.html). | ||||
|   and do the Git tutorial (coming soon) if you are unfamiliar with Git, | ||||
|   getting help in | ||||
|   [#git help](https://chat.zulip.org/#narrow/stream/44-git-help) if you run | ||||
|   into any troubles. | ||||
| * Sign the | ||||
|   [Dropbox Contributor License Agreement](https://opensource.dropbox.com/cla/). | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ RUN useradd -d /home/zulip -m zulip && echo 'zulip ALL=(ALL) NOPASSWD:ALL' >> /e | ||||
| USER zulip | ||||
|  | ||||
| RUN ln -nsf /srv/zulip ~/zulip | ||||
|  | ||||
| RUN echo 'export LC_ALL="en_US.UTF-8" LANG="en_US.UTF-8" LANGUAGE="en_US.UTF-8"' >> ~zulip/.bashrc | ||||
| RUN echo 'export LC_ALL="en_US.UTF-8" LANG="en_US.UTF-8" LANGUAGE="en_US.UTF-8"' >> ~zulip/.bash_profile | ||||
|  | ||||
| WORKDIR /srv/zulip | ||||
|   | ||||
| @@ -8,11 +8,10 @@ allows users to easily process hundreds or thousands of messages a day. With | ||||
| over 300 contributors merging over 500 commits a month, Zulip is also the | ||||
| largest and fastest growing open source group chat project. | ||||
|  | ||||
| [](https://circleci.com/gh/zulip/zulip) | ||||
| [](https://circleci.com/gh/zulip/zulip) | ||||
| [](https://travis-ci.org/zulip/zulip) | ||||
| [](https://codecov.io/gh/zulip/zulip) | ||||
| [][mypy-coverage] | ||||
| [](https://github.com/zulip/zulip/releases/latest) | ||||
| [](https://zulip.readthedocs.io/en/latest/) | ||||
| [](https://chat.zulip.org) | ||||
| [](https://twitter.com/zulip) | ||||
| @@ -59,7 +58,7 @@ You might be interested in: | ||||
| * **Running a Zulip server**. Setting up a server takes just a couple | ||||
|   of minutes. Zulip runs on Ubuntu 18.04 Bionic, Ubuntu 16.04 Xenial, | ||||
|   Ubuntu 14.04 Trusty, and Debian 9 Stretch. The installation process is | ||||
|   [documented here](https://zulip.readthedocs.io/en/stable/production/install.html). | ||||
|   [documented here](https://zulip.readthedocs.io/en/stable/prod.html). | ||||
|   Commercial support is available; see <https://zulipchat.com/plans> | ||||
|   for details. | ||||
|  | ||||
|   | ||||
| @@ -2,14 +2,14 @@ import time | ||||
| from collections import OrderedDict, defaultdict | ||||
| from datetime import datetime, timedelta | ||||
| import logging | ||||
| from typing import Callable, Dict, List, \ | ||||
| from typing import Any, Callable, Dict, List, \ | ||||
|     Optional, Tuple, Type, Union | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.db import connection | ||||
| from django.db import connection, models | ||||
| from django.db.models import F | ||||
|  | ||||
| from analytics.models import BaseCount, \ | ||||
| from analytics.models import Anomaly, BaseCount, \ | ||||
|     FillState, InstallationCount, RealmCount, StreamCount, \ | ||||
|     UserCount, installation_epoch, last_successful_fill | ||||
| from zerver.lib.logging_util import log_to_file | ||||
| @@ -226,6 +226,7 @@ def do_drop_all_analytics_tables() -> None: | ||||
|     RealmCount.objects.all().delete() | ||||
|     InstallationCount.objects.all().delete() | ||||
|     FillState.objects.all().delete() | ||||
|     Anomaly.objects.all().delete() | ||||
|  | ||||
| def do_drop_single_stat(property: str) -> None: | ||||
|     UserCount.objects.filter(property=property).delete() | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| from argparse import ArgumentParser | ||||
| from datetime import timedelta | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils.timezone import now as timezone_now | ||||
|  | ||||
| from analytics.models import installation_epoch, \ | ||||
| from analytics.models import InstallationCount, installation_epoch, \ | ||||
|     last_successful_fill | ||||
| from analytics.lib.counts import COUNT_STATS, CountStat | ||||
| from zerver.lib.timestamp import floor_to_hour, floor_to_day, verify_UTC, \ | ||||
| @@ -11,6 +12,7 @@ from zerver.lib.timestamp import floor_to_hour, floor_to_day, verify_UTC, \ | ||||
| from zerver.models import Realm | ||||
|  | ||||
| import os | ||||
| import sys | ||||
| import time | ||||
| from typing import Any, Dict | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
|  | ||||
| from datetime import datetime, timedelta | ||||
| from typing import Any, Dict, List, Mapping, Optional, Type | ||||
| from typing import Any, Dict, List, Mapping, Optional, Type, Union | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils.timezone import now as timezone_now | ||||
| @@ -11,10 +11,10 @@ from analytics.lib.fixtures import generate_time_series_data | ||||
| from analytics.lib.time_utils import time_range | ||||
| from analytics.models import BaseCount, FillState, RealmCount, UserCount, \ | ||||
|     StreamCount, InstallationCount | ||||
| from zerver.lib.actions import do_change_is_admin, STREAM_ASSIGNMENT_COLORS | ||||
| from zerver.lib.actions import do_change_is_admin | ||||
| from zerver.lib.timestamp import floor_to_day | ||||
| from zerver.models import Realm, UserProfile, Stream, Client, \ | ||||
|     RealmAuditLog, Recipient, Subscription | ||||
| from zerver.models import Realm, UserProfile, Stream, Message, Client, \ | ||||
|     RealmAuditLog, Recipient | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = """Populates analytics tables with randomly generated data.""" | ||||
| @@ -72,16 +72,7 @@ class Command(BaseCommand): | ||||
|         do_change_is_admin(shylock, True) | ||||
|         stream = Stream.objects.create( | ||||
|             name='all', realm=realm, date_created=installation_time) | ||||
|         recipient = Recipient.objects.create(type_id=stream.id, type=Recipient.STREAM) | ||||
|  | ||||
|         # Subscribe shylock to the stream to avoid invariant failures. | ||||
|         # TODO: This should use subscribe_users_to_streams from populate_db. | ||||
|         subs = [ | ||||
|             Subscription(recipient=recipient, | ||||
|                          user_profile=shylock, | ||||
|                          color=STREAM_ASSIGNMENT_COLORS[0]), | ||||
|         ] | ||||
|         Subscription.objects.bulk_create(subs) | ||||
|         Recipient.objects.create(type_id=stream.id, type=Recipient.STREAM) | ||||
|  | ||||
|         def insert_fixture_data(stat: CountStat, | ||||
|                                 fixture_data: Mapping[Optional[str], List[int]], | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import datetime | ||||
| from argparse import ArgumentParser | ||||
| from typing import Any, List | ||||
|  | ||||
| import pytz | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.db.models import Count | ||||
| from django.utils.timezone import now as timezone_now | ||||
|   | ||||
| @@ -25,33 +25,20 @@ class Command(BaseCommand): | ||||
|             realms = Realm.objects.all() | ||||
|  | ||||
|         for realm in realms: | ||||
|             print(realm.string_id) | ||||
|             print("------------") | ||||
|             print("%25s %15s %10s" % ("stream", "subscribers", "messages")) | ||||
|             streams = Stream.objects.filter(realm=realm).exclude(Q(name__istartswith="tutorial-")) | ||||
|             # private stream count | ||||
|             private_count = 0 | ||||
|             # public stream count | ||||
|             public_count = 0 | ||||
|             invite_only_count = 0 | ||||
|             for stream in streams: | ||||
|                 if stream.invite_only: | ||||
|                     private_count += 1 | ||||
|                 else: | ||||
|                     public_count += 1 | ||||
|             print("------------") | ||||
|             print(realm.string_id, end=' ') | ||||
|             print("%10s %d public streams and" % ("(", public_count), end=' ') | ||||
|             print("%d private streams )" % (private_count,)) | ||||
|             print("------------") | ||||
|             print("%25s %15s %10s %12s" % ("stream", "subscribers", "messages", "type")) | ||||
|  | ||||
|             for stream in streams: | ||||
|                 if stream.invite_only: | ||||
|                     stream_type = 'private' | ||||
|                 else: | ||||
|                     stream_type = 'public' | ||||
|                     invite_only_count += 1 | ||||
|                     continue | ||||
|                 print("%25s" % (stream.name,), end=' ') | ||||
|                 recipient = Recipient.objects.filter(type=Recipient.STREAM, type_id=stream.id) | ||||
|                 print("%10d" % (len(Subscription.objects.filter(recipient=recipient, | ||||
|                                                                 active=True)),), end=' ') | ||||
|                 num_messages = len(Message.objects.filter(recipient=recipient)) | ||||
|                 print("%12d" % (num_messages,), end=' ') | ||||
|                 print("%15s" % (stream_type,)) | ||||
|                 print("%12d" % (num_messages,)) | ||||
|             print("%d private streams" % (invite_only_count,)) | ||||
|             print("") | ||||
|   | ||||
| @@ -3,6 +3,8 @@ import django.db.models.deletion | ||||
| from django.conf import settings | ||||
| from django.db import migrations, models | ||||
|  | ||||
| import zerver.lib.str_utils | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| from django.db import migrations | ||||
| from django.db import migrations, models | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| from django.db import migrations, models | ||||
|  | ||||
| import zerver.lib.str_utils | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| from django.db import migrations | ||||
| from django.db import migrations, models | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.10.4 on 2017-01-16 20:50 | ||||
| from django.conf import settings | ||||
| from django.db import migrations | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|   | ||||
| @@ -1,34 +0,0 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.11.18 on 2019-02-02 02:47 | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('analytics', '0012_add_on_delete'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name='installationcount', | ||||
|             name='anomaly', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='realmcount', | ||||
|             name='anomaly', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='streamcount', | ||||
|             name='anomaly', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='usercount', | ||||
|             name='anomaly', | ||||
|         ), | ||||
|         migrations.DeleteModel( | ||||
|             name='Anomaly', | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,10 +1,10 @@ | ||||
| import datetime | ||||
| from typing import Optional | ||||
| from typing import Any, Dict, Optional, Tuple, Union | ||||
|  | ||||
| from django.db import models | ||||
|  | ||||
| from zerver.lib.timestamp import floor_to_day | ||||
| from zerver.models import Realm, Stream, UserProfile | ||||
| from zerver.models import Realm, Recipient, Stream, UserProfile | ||||
|  | ||||
| class FillState(models.Model): | ||||
|     property = models.CharField(max_length=40, unique=True)  # type: str | ||||
| @@ -34,6 +34,13 @@ def last_successful_fill(property: str) -> Optional[datetime.datetime]: | ||||
|         return fillstate.end_time | ||||
|     return fillstate.end_time - datetime.timedelta(hours=1) | ||||
|  | ||||
| # would only ever make entries here by hand | ||||
| class Anomaly(models.Model): | ||||
|     info = models.CharField(max_length=1000)  # type: str | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return "<Anomaly: %s... %s>" % (self.info, self.id) | ||||
|  | ||||
| class BaseCount(models.Model): | ||||
|     # Note: When inheriting from BaseCount, you may want to rearrange | ||||
|     # the order of the columns in the migration to make sure they | ||||
| @@ -42,6 +49,7 @@ class BaseCount(models.Model): | ||||
|     subgroup = models.CharField(max_length=16, null=True)  # type: Optional[str] | ||||
|     end_time = models.DateTimeField()  # type: datetime.datetime | ||||
|     value = models.BigIntegerField()  # type: int | ||||
|     anomaly = models.ForeignKey(Anomaly, on_delete=models.SET_NULL, null=True)  # type: Optional[Anomaly] | ||||
|  | ||||
|     class Meta: | ||||
|         abstract = True | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
|  | ||||
| from datetime import datetime, timedelta | ||||
| from typing import Any, Dict, List, Optional, Tuple, Type | ||||
| from typing import Any, Dict, List, Optional, Tuple, Type, Union | ||||
|  | ||||
| import ujson | ||||
| from django.apps import apps | ||||
| @@ -10,20 +10,19 @@ from django.test import TestCase | ||||
| from django.utils.timezone import now as timezone_now | ||||
| from django.utils.timezone import utc as timezone_utc | ||||
|  | ||||
| from analytics.lib.counts import COUNT_STATS, CountStat, \ | ||||
| from analytics.lib.counts import COUNT_STATS, CountStat, DataCollector, \ | ||||
|     DependentCountStat, LoggingCountStat, do_aggregate_to_summary_table, \ | ||||
|     do_drop_all_analytics_tables, do_drop_single_stat, \ | ||||
|     do_fill_count_stat_at_hour, do_increment_logging_stat, \ | ||||
|     process_count_stat, sql_data_collector | ||||
| from analytics.models import BaseCount, \ | ||||
| from analytics.models import Anomaly, BaseCount, \ | ||||
|     FillState, InstallationCount, RealmCount, StreamCount, \ | ||||
|     UserCount, installation_epoch | ||||
|     UserCount, installation_epoch, last_successful_fill | ||||
| from zerver.lib.actions import do_activate_user, do_create_user, \ | ||||
|     do_deactivate_user, do_reactivate_user, update_user_activity_interval, \ | ||||
|     do_invite_users, do_revoke_user_invite, do_resend_user_invite_email, \ | ||||
|     InvitationError | ||||
| from zerver.lib.timestamp import TimezoneNotUTCException, floor_to_day | ||||
| from zerver.lib.topic import DB_TOPIC_NAME | ||||
| from zerver.models import Client, Huddle, Message, Realm, \ | ||||
|     RealmAuditLog, Recipient, Stream, UserActivityInterval, \ | ||||
|     UserProfile, get_client, get_user, PreregistrationUser | ||||
| @@ -57,10 +56,7 @@ class AnalyticsTestCase(TestCase): | ||||
|             'api_key': '42'} | ||||
|         for key, value in defaults.items(): | ||||
|             kwargs[key] = kwargs.get(key, value) | ||||
|         kwargs['delivery_email'] = kwargs['email'] | ||||
|         user_profile = UserProfile.objects.create(**kwargs) | ||||
|         # TODO: Make this pass user_profile.full_clean() | ||||
|         return user_profile | ||||
|         return UserProfile.objects.create(**kwargs) | ||||
|  | ||||
|     def create_stream_with_recipient(self, **kwargs: Any) -> Tuple[Stream, Recipient]: | ||||
|         self.name_counter += 1 | ||||
| @@ -86,7 +82,7 @@ class AnalyticsTestCase(TestCase): | ||||
|         defaults = { | ||||
|             'sender': sender, | ||||
|             'recipient': recipient, | ||||
|             DB_TOPIC_NAME: 'subject', | ||||
|             'subject': 'subject', | ||||
|             'content': 'hi', | ||||
|             'pub_date': self.TIME_LAST_HOUR, | ||||
|             'sending_client': get_client("website")} | ||||
| @@ -846,6 +842,7 @@ class TestDeleteStats(AnalyticsTestCase): | ||||
|         RealmCount.objects.create(realm=user.realm, **count_args) | ||||
|         InstallationCount.objects.create(**count_args) | ||||
|         FillState.objects.create(property='test', end_time=self.TIME_ZERO, state=FillState.DONE) | ||||
|         Anomaly.objects.create(info='test anomaly') | ||||
|  | ||||
|         analytics = apps.get_app_config('analytics') | ||||
|         for table in list(analytics.models.values()): | ||||
| @@ -868,6 +865,7 @@ class TestDeleteStats(AnalyticsTestCase): | ||||
|             InstallationCount.objects.create(**count_args) | ||||
|         FillState.objects.create(property='to_delete', end_time=self.TIME_ZERO, state=FillState.DONE) | ||||
|         FillState.objects.create(property='to_save', end_time=self.TIME_ZERO, state=FillState.DONE) | ||||
|         Anomaly.objects.create(info='test anomaly') | ||||
|  | ||||
|         analytics = apps.get_app_config('analytics') | ||||
|         for table in list(analytics.models.values()): | ||||
| @@ -875,8 +873,11 @@ class TestDeleteStats(AnalyticsTestCase): | ||||
|  | ||||
|         do_drop_single_stat('to_delete') | ||||
|         for table in list(analytics.models.values()): | ||||
|             self.assertFalse(table.objects.filter(property='to_delete').exists()) | ||||
|             self.assertTrue(table.objects.filter(property='to_save').exists()) | ||||
|             if table._meta.db_table == 'analytics_anomaly': | ||||
|                 self.assertTrue(table.objects.exists()) | ||||
|             else: | ||||
|                 self.assertFalse(table.objects.filter(property='to_delete').exists()) | ||||
|                 self.assertTrue(table.objects.filter(property='to_save').exists()) | ||||
|  | ||||
| class TestActiveUsersAudit(AnalyticsTestCase): | ||||
|     def setUp(self) -> None: | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| from datetime import datetime, timedelta | ||||
| from typing import List, Optional | ||||
| from typing import Dict, List, Optional | ||||
|  | ||||
| import mock | ||||
| from django.utils.timezone import utc | ||||
| @@ -8,8 +8,8 @@ from analytics.lib.counts import COUNT_STATS, CountStat | ||||
| from analytics.lib.time_utils import time_range | ||||
| from analytics.models import FillState, \ | ||||
|     RealmCount, UserCount, last_successful_fill | ||||
| from analytics.views import rewrite_client_arrays, \ | ||||
|     sort_by_totals, sort_client_labels | ||||
| from analytics.views import get_chart_data, rewrite_client_arrays, \ | ||||
|     sort_by_totals, sort_client_labels, stats | ||||
| from zerver.lib.test_classes import ZulipTestCase | ||||
| from zerver.lib.timestamp import ceiling_to_day, \ | ||||
|     ceiling_to_hour, datetime_to_timestamp | ||||
|   | ||||
| @@ -16,12 +16,6 @@ i18n_urlpatterns = [ | ||||
|         name='analytics.views.stats_for_realm'), | ||||
|     url(r'^stats/installation$', analytics.views.stats_for_installation, | ||||
|         name='analytics.views.stats_for_installation'), | ||||
|     url(r'^stats/remote/(?P<remote_server_id>[\S]+)/installation$', | ||||
|         analytics.views.stats_for_remote_installation, | ||||
|         name='analytics.views.stats_for_remote_installation'), | ||||
|     url(r'^stats/remote/(?P<remote_server_id>[\S]+)/realm/(?P<remote_realm_id>[\S]+)/$', | ||||
|         analytics.views.stats_for_remote_realm, | ||||
|         name='analytics.views.stats_for_remote_realm'), | ||||
|  | ||||
|     # User-visible stats page | ||||
|     url(r'^stats$', analytics.views.stats, | ||||
| @@ -44,11 +38,6 @@ v1_api_and_json_patterns = [ | ||||
|         {'GET': 'analytics.views.get_chart_data_for_realm'}), | ||||
|     url(r'^analytics/chart_data/installation$', rest_dispatch, | ||||
|         {'GET': 'analytics.views.get_chart_data_for_installation'}), | ||||
|     url(r'^analytics/chart_data/remote/(?P<remote_server_id>[\S]+)/installation$', rest_dispatch, | ||||
|         {'GET': 'analytics.views.get_chart_data_for_remote_installation'}), | ||||
|     url(r'^analytics/chart_data/remote/(?P<remote_server_id>[\S]+)/realm/(?P<remote_realm_id>[\S]+)$', | ||||
|         rest_dispatch, | ||||
|         {'GET': 'analytics.views.get_chart_data_for_remote_realm'}), | ||||
| ] | ||||
|  | ||||
| i18n_urlpatterns += [ | ||||
|   | ||||
| @@ -1,26 +1,28 @@ | ||||
|  | ||||
| import itertools | ||||
| import json | ||||
| import logging | ||||
| import re | ||||
| import time | ||||
| from collections import defaultdict | ||||
| from datetime import datetime, timedelta | ||||
| from typing import Any, Callable, Dict, List, \ | ||||
|     Optional, Set, Tuple, Type, Union, cast | ||||
|     Optional, Set, Tuple, Type, Union | ||||
|  | ||||
| import pytz | ||||
| from django.conf import settings | ||||
| from django.urls import reverse | ||||
| from django.db import connection | ||||
| from django.db.models import Sum | ||||
| from django.db.models.query import QuerySet | ||||
| from django.http import HttpRequest, HttpResponse, HttpResponseNotFound | ||||
| from django.shortcuts import render | ||||
| from django.template import loader | ||||
| from django.template import RequestContext, loader | ||||
| from django.utils.timezone import now as timezone_now, utc as timezone_utc | ||||
| from django.utils.translation import ugettext as _ | ||||
| from jinja2 import Markup as mark_safe | ||||
|  | ||||
| from analytics.lib.counts import COUNT_STATS, CountStat | ||||
| from analytics.lib.counts import COUNT_STATS, CountStat, process_count_stat | ||||
| from analytics.lib.time_utils import time_range | ||||
| from analytics.models import BaseCount, InstallationCount, \ | ||||
|     RealmCount, StreamCount, UserCount, last_successful_fill, installation_epoch | ||||
| @@ -30,25 +32,16 @@ from zerver.lib.exceptions import JsonableError | ||||
| from zerver.lib.json_encoder_for_html import JSONEncoderForHTML | ||||
| from zerver.lib.request import REQ, has_request_variables | ||||
| from zerver.lib.response import json_success | ||||
| from zerver.lib.timestamp import convert_to_UTC, timestamp_to_datetime | ||||
| from zerver.lib.timestamp import ceiling_to_day, \ | ||||
|     ceiling_to_hour, convert_to_UTC, timestamp_to_datetime | ||||
| from zerver.models import Client, get_realm, Realm, \ | ||||
|     UserActivity, UserActivityInterval, UserProfile | ||||
|  | ||||
| if settings.ZILENCER_ENABLED: | ||||
|     from zilencer.models import RemoteInstallationCount, RemoteRealmCount, \ | ||||
|         RemoteZulipServer | ||||
| else: | ||||
|     from mock import Mock | ||||
|     RemoteInstallationCount = Mock()  # type: ignore # https://github.com/JukkaL/mypy/issues/1188 | ||||
|     RemoteZulipServer = Mock()  # type: ignore # https://github.com/JukkaL/mypy/issues/1188 | ||||
|     RemoteRealmCount = Mock()  # type: ignore # https://github.com/JukkaL/mypy/issues/1188 | ||||
|  | ||||
| def render_stats(request: HttpRequest, data_url_suffix: str, target_name: str, | ||||
|                  for_installation: bool=False, remote: bool=False) -> HttpRequest: | ||||
|                  for_installation: bool=False) -> HttpRequest: | ||||
|     page_params = dict( | ||||
|         data_url_suffix=data_url_suffix, | ||||
|         for_installation=for_installation, | ||||
|         remote=remote, | ||||
|         debug_mode=False, | ||||
|     ) | ||||
|     return render(request, | ||||
| @@ -74,14 +67,6 @@ def stats_for_realm(request: HttpRequest, realm_str: str) -> HttpResponse: | ||||
|  | ||||
|     return render_stats(request, '/realm/%s' % (realm_str,), realm.name or realm.string_id) | ||||
|  | ||||
| @require_server_admin | ||||
| @has_request_variables | ||||
| def stats_for_remote_realm(request: HttpRequest, remote_server_id: str, | ||||
|                            remote_realm_id: str) -> HttpResponse: | ||||
|     server = RemoteZulipServer.objects.get(id=remote_server_id) | ||||
|     return render_stats(request, '/remote/%s/realm/%s' % (server.id, remote_realm_id), | ||||
|                         "Realm %s on server %s" % (remote_realm_id, server.hostname)) | ||||
|  | ||||
| @require_server_admin_api | ||||
| @has_request_variables | ||||
| def get_chart_data_for_realm(request: HttpRequest, user_profile: UserProfile, | ||||
| @@ -92,65 +77,26 @@ def get_chart_data_for_realm(request: HttpRequest, user_profile: UserProfile, | ||||
|  | ||||
|     return get_chart_data(request=request, user_profile=user_profile, realm=realm, **kwargs) | ||||
|  | ||||
| @require_server_admin_api | ||||
| @has_request_variables | ||||
| def get_chart_data_for_remote_realm( | ||||
|         request: HttpRequest, user_profile: UserProfile, remote_server_id: str, | ||||
|         remote_realm_id: str, **kwargs: Any) -> HttpResponse: | ||||
|     server = RemoteZulipServer.objects.get(id=remote_server_id) | ||||
|     return get_chart_data(request=request, user_profile=user_profile, server=server, | ||||
|                           remote=True, remote_realm_id=int(remote_realm_id), **kwargs) | ||||
|  | ||||
| @require_server_admin | ||||
| def stats_for_installation(request: HttpRequest) -> HttpResponse: | ||||
|     return render_stats(request, '/installation', 'Installation', True) | ||||
|  | ||||
| @require_server_admin | ||||
| def stats_for_remote_installation(request: HttpRequest, remote_server_id: str) -> HttpResponse: | ||||
|     server = RemoteZulipServer.objects.get(id=remote_server_id) | ||||
|     return render_stats(request, '/remote/%s/installation' % (server.id,), | ||||
|                         'remote Installation %s' % (server.hostname), True, True) | ||||
|  | ||||
| @require_server_admin_api | ||||
| @has_request_variables | ||||
| def get_chart_data_for_installation(request: HttpRequest, user_profile: UserProfile, | ||||
|                                     chart_name: str=REQ(), **kwargs: Any) -> HttpResponse: | ||||
|     return get_chart_data(request=request, user_profile=user_profile, for_installation=True, **kwargs) | ||||
|  | ||||
| @require_server_admin_api | ||||
| @has_request_variables | ||||
| def get_chart_data_for_remote_installation( | ||||
|         request: HttpRequest, | ||||
|         user_profile: UserProfile, | ||||
|         remote_server_id: str, | ||||
|         chart_name: str=REQ(), | ||||
|         **kwargs: Any) -> HttpResponse: | ||||
|     server = RemoteZulipServer.objects.get(id=remote_server_id) | ||||
|     return get_chart_data(request=request, user_profile=user_profile, for_installation=True, | ||||
|                           remote=True, server=server, **kwargs) | ||||
|  | ||||
| @require_non_guest_user | ||||
| @has_request_variables | ||||
| def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name: str=REQ(), | ||||
|                    min_length: Optional[int]=REQ(converter=to_non_negative_int, default=None), | ||||
|                    start: Optional[datetime]=REQ(converter=to_utc_datetime, default=None), | ||||
|                    end: Optional[datetime]=REQ(converter=to_utc_datetime, default=None), | ||||
|                    realm: Optional[Realm]=None, for_installation: bool=False, | ||||
|                    remote: bool=False, remote_realm_id: Optional[int]=None, | ||||
|                    server: Optional[RemoteZulipServer]=None) -> HttpResponse: | ||||
|                    realm: Optional[Realm]=None, for_installation: bool=False) -> HttpResponse: | ||||
|     aggregate_table = RealmCount | ||||
|     if for_installation: | ||||
|         if remote: | ||||
|             aggregate_table = RemoteInstallationCount | ||||
|             assert server is not None | ||||
|         else: | ||||
|             aggregate_table = InstallationCount | ||||
|     else: | ||||
|         if remote: | ||||
|             aggregate_table = RemoteRealmCount | ||||
|             assert server is not None | ||||
|             assert remote_realm_id is not None | ||||
|         else: | ||||
|             aggregate_table = RealmCount | ||||
|         aggregate_table = InstallationCount | ||||
|  | ||||
|     if chart_name == 'number_of_humans': | ||||
|         stats = [ | ||||
| @@ -201,60 +147,29 @@ def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name: | ||||
|                             {'start': start, 'end': end}) | ||||
|  | ||||
|     if realm is None: | ||||
|         # Note that this value is invalid for Remote tables; be | ||||
|         # careful not to access it in those code paths. | ||||
|         realm = user_profile.realm | ||||
|  | ||||
|     if remote: | ||||
|         # For remote servers, we don't have fillstate data, and thus | ||||
|         # should simply use the first and last data points for the | ||||
|         # table. | ||||
|         assert server is not None | ||||
|         if not aggregate_table.objects.filter(server=server).exists(): | ||||
|             raise JsonableError(_("No analytics data available. Please contact your server administrator.")) | ||||
|         if start is None: | ||||
|             start = aggregate_table.objects.filter(server=server).first().end_time | ||||
|         if end is None: | ||||
|             end = aggregate_table.objects.filter(server=server).last().end_time | ||||
|     else: | ||||
|         # Otherwise, we can use tables on the current server to | ||||
|         # determine a nice range, and some additional validation. | ||||
|         if start is None: | ||||
|             if for_installation: | ||||
|                 start = installation_epoch() | ||||
|             else: | ||||
|                 start = realm.date_created | ||||
|         if end is None: | ||||
|             end = max(last_successful_fill(stat.property) or | ||||
|                       datetime.min.replace(tzinfo=timezone_utc) for stat in stats) | ||||
|         if end is None or start > end: | ||||
|             logging.warning("User from realm %s attempted to access /stats, but the computed " | ||||
|                             "start time: %s (creation of realm or installation) is later than the computed " | ||||
|                             "end time: %s (last successful analytics update). Is the " | ||||
|                             "analytics cron job running?" % (realm.string_id, start, end)) | ||||
|             raise JsonableError(_("No analytics data available. Please contact your server administrator.")) | ||||
|     if start is None: | ||||
|         if for_installation: | ||||
|             start = installation_epoch() | ||||
|         else: | ||||
|             start = realm.date_created | ||||
|     if end is None: | ||||
|         end = max(last_successful_fill(stat.property) or | ||||
|                   datetime.min.replace(tzinfo=timezone_utc) for stat in stats) | ||||
|     if end is None or start > end: | ||||
|         logging.warning("User from realm %s attempted to access /stats, but the computed " | ||||
|                         "start time: %s (creation of realm or installation) is later than the computed " | ||||
|                         "end time: %s (last successful analytics update). Is the " | ||||
|                         "analytics cron job running?" % (realm.string_id, start, end)) | ||||
|         raise JsonableError(_("No analytics data available. Please contact your server administrator.")) | ||||
|  | ||||
|     assert len(set([stat.frequency for stat in stats])) == 1 | ||||
|     end_times = time_range(start, end, stats[0].frequency, min_length) | ||||
|     data = {'end_times': end_times, 'frequency': stats[0].frequency}  # type: Dict[str, Any] | ||||
|  | ||||
|     aggregation_level = { | ||||
|         InstallationCount: 'everyone', | ||||
|         RealmCount: 'everyone', | ||||
|         RemoteInstallationCount: 'everyone', | ||||
|         RemoteRealmCount: 'everyone', | ||||
|         UserCount: 'user', | ||||
|     } | ||||
|     aggregation_level = {InstallationCount: 'everyone', RealmCount: 'everyone', UserCount: 'user'} | ||||
|     # -1 is a placeholder value, since there is no relevant filtering on InstallationCount | ||||
|     id_value = { | ||||
|         InstallationCount: -1, | ||||
|         RealmCount: realm.id, | ||||
|         RemoteInstallationCount: cast(RemoteZulipServer, server).id if server is not None else None, | ||||
|         # TODO: RemoteRealmCount logic doesn't correctly handle | ||||
|         # filtering by server_id as well. | ||||
|         RemoteRealmCount: remote_realm_id, | ||||
|         UserCount: user_profile.id, | ||||
|     } | ||||
|     id_value = {InstallationCount: -1, RealmCount: realm.id, UserCount: user_profile.id} | ||||
|     for table in tables: | ||||
|         data[aggregation_level[table]] = {} | ||||
|         for stat in stats: | ||||
| @@ -298,10 +213,6 @@ def table_filtered_to_id(table: Type[BaseCount], key_id: int) -> QuerySet: | ||||
|         return StreamCount.objects.filter(stream_id=key_id) | ||||
|     elif table == InstallationCount: | ||||
|         return InstallationCount.objects.all() | ||||
|     elif table == RemoteInstallationCount: | ||||
|         return RemoteInstallationCount.objects.filter(server_id=key_id) | ||||
|     elif table == RemoteRealmCount: | ||||
|         return RemoteRealmCount.objects.filter(realm_id=key_id) | ||||
|     else: | ||||
|         raise AssertionError("Unknown table: %s" % (table,)) | ||||
|  | ||||
| @@ -565,7 +476,7 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str: | ||||
|     for row in rows: | ||||
|         row['date_created_day'] = row['date_created'].strftime('%Y-%m-%d') | ||||
|         row['plan_type_string'] = [ | ||||
|             '', 'self hosted', 'limited', 'standard', 'open source'][row['plan_type']] | ||||
|             '', 'self hosted', 'limited', 'standard', 'standard free'][row['plan_type']] | ||||
|         row['age_days'] = int((now - row['date_created']).total_seconds() | ||||
|                               / 86400) | ||||
|         row['is_new'] = row['age_days'] < 12 * 7 | ||||
| @@ -579,16 +490,6 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str: | ||||
|         except Exception: | ||||
|             row['history'] = '' | ||||
|  | ||||
|     # estimate annual subscription revenue | ||||
|     total_amount = 0 | ||||
|     if settings.BILLING_ENABLED: | ||||
|         from corporate.lib.stripe import estimate_annual_recurring_revenue_by_realm | ||||
|         estimated_arrs = estimate_annual_recurring_revenue_by_realm() | ||||
|         for row in rows: | ||||
|             if row['string_id'] in estimated_arrs: | ||||
|                 row['amount'] = estimated_arrs[row['string_id']] | ||||
|         total_amount += sum(estimated_arrs.values()) | ||||
|  | ||||
|     # augment data with realm_minutes | ||||
|     total_hours = 0.0 | ||||
|     for row in rows: | ||||
| @@ -624,10 +525,9 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str: | ||||
|         total_bot_count += int(row['bot_count']) | ||||
|         total_wau_count += int(row['wau_count']) | ||||
|  | ||||
|     total_row = dict( | ||||
|     rows.append(dict( | ||||
|         string_id='Total', | ||||
|         plan_type_string="", | ||||
|         amount=total_amount, | ||||
|         stats_link = '', | ||||
|         date_created_day='', | ||||
|         realm_admin_email='', | ||||
| @@ -636,9 +536,7 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str: | ||||
|         bot_count=total_bot_count, | ||||
|         hours=int(total_hours), | ||||
|         wau_count=total_wau_count, | ||||
|     ) | ||||
|  | ||||
|     rows.insert(0, total_row) | ||||
|     )) | ||||
|  | ||||
|     content = loader.render_to_string( | ||||
|         'analytics/realm_summary_table.html', | ||||
| @@ -664,15 +562,15 @@ def user_activity_intervals() -> Tuple[mark_safe, Dict[str, float]]: | ||||
|     ).only( | ||||
|         'start', | ||||
|         'end', | ||||
|         'user_profile__delivery_email', | ||||
|         'user_profile__email', | ||||
|         'user_profile__realm__string_id' | ||||
|     ).order_by( | ||||
|         'user_profile__realm__string_id', | ||||
|         'user_profile__delivery_email' | ||||
|         'user_profile__email' | ||||
|     ) | ||||
|  | ||||
|     by_string_id = lambda row: row.user_profile.realm.string_id | ||||
|     by_email = lambda row: row.user_profile.delivery_email | ||||
|     by_email = lambda row: row.user_profile.email | ||||
|  | ||||
|     realm_minutes = {} | ||||
|  | ||||
| @@ -766,8 +664,7 @@ def sent_messages_report(realm: str) -> str: | ||||
|     return make_table(title, cols, rows) | ||||
|  | ||||
| def ad_hoc_queries() -> List[Dict[str, str]]: | ||||
|     def get_page(query: str, cols: List[str], title: str, | ||||
|                  totals_columns: List[int]=[]) -> Dict[str, str]: | ||||
|     def get_page(query: str, cols: List[str], title: str) -> Dict[str, str]: | ||||
|         cursor = connection.cursor() | ||||
|         cursor.execute(query) | ||||
|         rows = cursor.fetchall() | ||||
| @@ -779,24 +676,11 @@ def ad_hoc_queries() -> List[Dict[str, str]]: | ||||
|             for row in rows: | ||||
|                 row[i] = fixup_func(row[i]) | ||||
|  | ||||
|         total_row = [] | ||||
|         for i, col in enumerate(cols): | ||||
|             if col == 'Realm': | ||||
|                 fix_rows(i, realm_activity_link) | ||||
|             elif col in ['Last time', 'Last visit']: | ||||
|                 fix_rows(i, format_date_for_activity_reports) | ||||
|             elif col == 'Hostname': | ||||
|                 for row in rows: | ||||
|                     row[i] = remote_installation_stats_link(row[0], row[i]) | ||||
|             if len(totals_columns) > 0: | ||||
|                 if i == 0: | ||||
|                     total_row.append("Total") | ||||
|                 elif i in totals_columns: | ||||
|                     total_row.append(str(sum(row[i] for row in rows if row[i] is not None))) | ||||
|                 else: | ||||
|                     total_row.append('') | ||||
|         if len(totals_columns) > 0: | ||||
|             rows.insert(0, total_row) | ||||
|  | ||||
|         content = make_table(title, cols, rows) | ||||
|  | ||||
| @@ -946,49 +830,6 @@ def ad_hoc_queries() -> List[Dict[str, str]]: | ||||
|  | ||||
|     pages.append(get_page(query, cols, title)) | ||||
|  | ||||
|     title = 'Remote Zulip servers' | ||||
|  | ||||
|     query = ''' | ||||
|         with icount as ( | ||||
|             select | ||||
|                 server_id, | ||||
|                 max(value) as max_value, | ||||
|                 max(end_time) as max_end_time | ||||
|             from zilencer_remoteinstallationcount | ||||
|             where | ||||
|                 property='active_users:is_bot:day' | ||||
|                 and subgroup='false' | ||||
|             group by server_id | ||||
|             ), | ||||
|         remote_push_devices as ( | ||||
|             select server_id, count(distinct(user_id)) as push_user_count from zilencer_remotepushdevicetoken | ||||
|             group by server_id | ||||
|         ) | ||||
|         select | ||||
|             rserver.id, | ||||
|             rserver.hostname, | ||||
|             rserver.contact_email, | ||||
|             max_value, | ||||
|             push_user_count, | ||||
|             max_end_time | ||||
|         from zilencer_remotezulipserver rserver | ||||
|         left join icount on icount.server_id = rserver.id | ||||
|         left join remote_push_devices on remote_push_devices.server_id = rserver.id | ||||
|         order by max_value DESC NULLS LAST, push_user_count DESC NULLS LAST | ||||
|     ''' | ||||
|  | ||||
|     cols = [ | ||||
|         'ID', | ||||
|         'Hostname', | ||||
|         'Contact email', | ||||
|         'Analytics users', | ||||
|         'Mobile users', | ||||
|         'Last update time', | ||||
|     ] | ||||
|  | ||||
|     pages.append(get_page(query, cols, title, | ||||
|                           totals_columns=[3, 4])) | ||||
|  | ||||
|     return pages | ||||
|  | ||||
| @require_server_admin | ||||
| @@ -1014,7 +855,7 @@ def get_activity(request: HttpRequest) -> HttpResponse: | ||||
| def get_user_activity_records_for_realm(realm: str, is_bot: bool) -> QuerySet: | ||||
|     fields = [ | ||||
|         'user_profile__full_name', | ||||
|         'user_profile__delivery_email', | ||||
|         'user_profile__email', | ||||
|         'query', | ||||
|         'client__name', | ||||
|         'count', | ||||
| @@ -1026,7 +867,7 @@ def get_user_activity_records_for_realm(realm: str, is_bot: bool) -> QuerySet: | ||||
|         user_profile__is_active=True, | ||||
|         user_profile__is_bot=is_bot | ||||
|     ) | ||||
|     records = records.order_by("user_profile__delivery_email", "-last_visit") | ||||
|     records = records.order_by("user_profile__email", "-last_visit") | ||||
|     records = records.select_related('user_profile', 'client').only(*fields) | ||||
|     return records | ||||
|  | ||||
| @@ -1040,7 +881,7 @@ def get_user_activity_records_for_email(email: str) -> List[QuerySet]: | ||||
|     ] | ||||
|  | ||||
|     records = UserActivity.objects.filter( | ||||
|         user_profile__delivery_email=email | ||||
|         user_profile__email=email | ||||
|     ) | ||||
|     records = records.order_by("-last_visit") | ||||
|     records = records.select_related('user_profile', 'client').only(*fields) | ||||
| @@ -1139,12 +980,6 @@ def realm_stats_link(realm_str: str) -> mark_safe: | ||||
|     stats_link = '<a href="{}"><i class="fa fa-pie-chart"></i></a>'.format(url, realm_str) | ||||
|     return mark_safe(stats_link) | ||||
|  | ||||
| def remote_installation_stats_link(server_id: int, hostname: str) -> mark_safe: | ||||
|     url_name = 'analytics.views.stats_for_remote_installation' | ||||
|     url = reverse(url_name, kwargs=dict(remote_server_id=server_id)) | ||||
|     stats_link = '<a href="{}"><i class="fa fa-pie-chart"></i>{}</a>'.format(url, hostname) | ||||
|     return mark_safe(stats_link) | ||||
|  | ||||
| def realm_client_table(user_summaries: Dict[str, Dict[str, Dict[str, Any]]]) -> str: | ||||
|     exclude_keys = [ | ||||
|         'internal', | ||||
| @@ -1220,7 +1055,7 @@ def realm_user_summary_table(all_records: List[QuerySet], | ||||
|     user_records = {} | ||||
|  | ||||
|     def by_email(record: QuerySet) -> str: | ||||
|         return record.user_profile.delivery_email | ||||
|         return record.user_profile.email | ||||
|  | ||||
|     for email, records in itertools.groupby(all_records, by_email): | ||||
|         user_records[email] = get_user_activity_summary(list(records)) | ||||
| @@ -1291,7 +1126,7 @@ def get_realm_activity(request: HttpRequest, realm_str: str) -> HttpResponse: | ||||
|     except Realm.DoesNotExist: | ||||
|         return HttpResponseNotFound("Realm %s does not exist" % (realm_str,)) | ||||
|  | ||||
|     admin_emails = {admin.delivery_email for admin in admins} | ||||
|     admin_emails = {admin.email for admin in admins} | ||||
|  | ||||
|     for is_bot, page_title in [(False, 'Humans'), (True, 'Bots')]: | ||||
|         all_records = list(get_user_activity_records_for_realm(realm_str, is_bot)) | ||||
|   | ||||
| @@ -16,11 +16,12 @@ from django.http import HttpRequest, HttpResponse | ||||
| from django.shortcuts import render | ||||
| from django.utils.timezone import now as timezone_now | ||||
|  | ||||
| from zerver.lib.utils import generate_random_token | ||||
| from zerver.models import PreregistrationUser, EmailChangeStatus, MultiuseInvite, \ | ||||
|     UserProfile, Realm | ||||
| from random import SystemRandom | ||||
| import string | ||||
| from typing import Dict, Optional, Union | ||||
| from typing import Any, Dict, Optional, Union | ||||
|  | ||||
| class ConfirmationKeyException(Exception): | ||||
|     WRONG_LENGTH = 1 | ||||
| @@ -69,11 +70,8 @@ def create_confirmation_link(obj: ContentType, host: str, | ||||
|                              confirmation_type: int, | ||||
|                              url_args: Optional[Dict[str, str]]=None) -> str: | ||||
|     key = generate_key() | ||||
|     realm = None | ||||
|     if hasattr(obj, 'realm'): | ||||
|         realm = obj.realm | ||||
|     Confirmation.objects.create(content_object=obj, date_sent=timezone_now(), confirmation_key=key, | ||||
|                                 realm=realm, type=confirmation_type) | ||||
|                                 realm=obj.realm, type=confirmation_type) | ||||
|     return confirmation_url(key, host, confirmation_type, url_args) | ||||
|  | ||||
| def confirmation_url(confirmation_key: str, host: str, | ||||
| @@ -101,7 +99,6 @@ class Confirmation(models.Model): | ||||
|     SERVER_REGISTRATION = 5 | ||||
|     MULTIUSE_INVITE = 6 | ||||
|     REALM_CREATION = 7 | ||||
|     REALM_REACTIVATION = 8 | ||||
|     type = models.PositiveSmallIntegerField()  # type: int | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
| @@ -124,18 +121,8 @@ _properties = { | ||||
|         'zerver.views.registration.accounts_home_from_multiuse_invite', | ||||
|         validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS), | ||||
|     Confirmation.REALM_CREATION: ConfirmationType('check_prereg_key_and_redirect'), | ||||
|     Confirmation.REALM_REACTIVATION: ConfirmationType('zerver.views.realm.realm_reactivation'), | ||||
| } | ||||
|  | ||||
| def one_click_unsubscribe_link(user_profile: UserProfile, email_type: str) -> str: | ||||
|     """ | ||||
|     Generate a unique link that a logged-out user can visit to unsubscribe from | ||||
|     Zulip e-mails without having to first log in. | ||||
|     """ | ||||
|     return create_confirmation_link(user_profile, user_profile.realm.host, | ||||
|                                     Confirmation.UNSUBSCRIBE, | ||||
|                                     url_args = {'email_type': email_type}) | ||||
|  | ||||
| # Functions related to links generated by the generate_realm_creation_link.py | ||||
| # management command. | ||||
| # Note that being validated here will just allow the user to access the create_realm | ||||
|   | ||||
| @@ -2,6 +2,8 @@ | ||||
|  | ||||
| # Copyright: (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com> | ||||
|  | ||||
| from typing import Any, Dict | ||||
|  | ||||
| __revision__ = '$Id: settings.py 12 2008-11-23 19:38:52Z jarek.zgoda $' | ||||
|  | ||||
| STATUS_ACTIVE = 1 | ||||
|   | ||||
| @@ -1,10 +1,8 @@ | ||||
| from datetime import datetime | ||||
| from decimal import Decimal | ||||
| import datetime | ||||
| from functools import wraps | ||||
| import logging | ||||
| import math | ||||
| import os | ||||
| from typing import Any, Callable, Dict, Optional, TypeVar, Tuple, cast | ||||
| from typing import Any, Callable, Dict, Optional, TypeVar, Tuple | ||||
| import ujson | ||||
|  | ||||
| from django.conf import settings | ||||
| @@ -14,12 +12,13 @@ from django.utils.timezone import now as timezone_now | ||||
| from django.core.signing import Signer | ||||
| import stripe | ||||
|  | ||||
| from zerver.lib.exceptions import JsonableError | ||||
| from zerver.lib.logging_util import log_to_file | ||||
| from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime | ||||
| from zerver.lib.utils import generate_random_token | ||||
| from zerver.lib.actions import do_change_plan_type | ||||
| from zerver.models import Realm, UserProfile, RealmAuditLog | ||||
| from corporate.models import Customer, CustomerPlan, LicenseLedger, \ | ||||
|     get_active_plan | ||||
| from corporate.models import Customer, Plan, Coupon, BillingProcessor | ||||
| from zproject.settings import get_secret | ||||
|  | ||||
| STRIPE_PUBLISHABLE_KEY = get_secret('stripe_publishable_key') | ||||
| @@ -33,17 +32,30 @@ billing_logger = logging.getLogger('corporate.stripe') | ||||
| log_to_file(billing_logger, BILLING_LOG_PATH) | ||||
| log_to_file(logging.getLogger('stripe'), BILLING_LOG_PATH) | ||||
|  | ||||
| ## Note: this is no longer accurate, as of when we added coupons | ||||
| # To generate the fixture data in stripe_fixtures.json: | ||||
| # * Set PRINT_STRIPE_FIXTURE_DATA to True | ||||
| # * ./manage.py setup_stripe | ||||
| # * Customer.objects.all().delete() | ||||
| # * Log in as a user, and go to http://localhost:9991/upgrade/ | ||||
| # * Click Add card. Enter the following billing details: | ||||
| #       Name: Ada Starr, Street: Under the sea, City: Pacific, | ||||
| #       Zip: 33333, Country: United States | ||||
| #       Card number: 4242424242424242, Expiry: 03/33, CVV: 333 | ||||
| # * Click Make payment. | ||||
| # * Copy out the 4 blobs of json from the dev console into stripe_fixtures.json. | ||||
| #   The contents of that file are '{\n' + concatenate the 4 json blobs + '\n}'. | ||||
| #   Then you can run e.g. `M-x mark-whole-buffer` and `M-x indent-region` in emacs | ||||
| #   to prettify the file (and make 4 space indents). | ||||
| # * Copy out the customer id, plan id, and quantity values into | ||||
| #   corporate.tests.test_stripe.StripeTest.setUp. | ||||
| # * Set PRINT_STRIPE_FIXTURE_DATA to False | ||||
| PRINT_STRIPE_FIXTURE_DATA = False | ||||
|  | ||||
| CallableT = TypeVar('CallableT', bound=Callable[..., Any]) | ||||
|  | ||||
| MIN_INVOICED_LICENSES = 30 | ||||
| DEFAULT_INVOICE_DAYS_UNTIL_DUE = 30 | ||||
|  | ||||
| def get_seat_count(realm: Realm) -> int: | ||||
|     non_guests = UserProfile.objects.filter( | ||||
|         realm=realm, is_active=True, is_bot=False, is_guest=False).count() | ||||
|     guests = UserProfile.objects.filter( | ||||
|         realm=realm, is_active=True, is_bot=False, is_guest=True).count() | ||||
|     return max(non_guests, math.ceil(guests / 5)) | ||||
|     return UserProfile.objects.filter(realm=realm, is_active=True, is_bot=False).count() | ||||
|  | ||||
| def sign_string(string: str) -> Tuple[str, str]: | ||||
|     salt = generate_random_token(64) | ||||
| @@ -54,78 +66,13 @@ def unsign_string(signed_string: str, salt: str) -> str: | ||||
|     signer = Signer(salt=salt) | ||||
|     return signer.unsign(signed_string) | ||||
|  | ||||
| # Be extremely careful changing this function. Historical billing periods | ||||
| # are not stored anywhere, and are just computed on the fly using this | ||||
| # function. Any change you make here should return the same value (or be | ||||
| # within a few seconds) for basically any value from when the billing system | ||||
| # went online to within a year from now. | ||||
| def add_months(dt: datetime, months: int) -> datetime: | ||||
|     assert(months >= 0) | ||||
|     # It's fine that the max day in Feb is 28 for leap years. | ||||
|     MAX_DAY_FOR_MONTH = {1: 31, 2: 28, 3: 31, 4: 30, 5: 31, 6: 30, | ||||
|                          7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31} | ||||
|     year = dt.year | ||||
|     month = dt.month + months | ||||
|     while month > 12: | ||||
|         year += 1 | ||||
|         month -= 12 | ||||
|     day = min(dt.day, MAX_DAY_FOR_MONTH[month]) | ||||
|     # datetimes don't support leap seconds, so don't need to worry about those | ||||
|     return dt.replace(year=year, month=month, day=day) | ||||
|  | ||||
| def next_month(billing_cycle_anchor: datetime, dt: datetime) -> datetime: | ||||
|     estimated_months = round((dt - billing_cycle_anchor).days * 12. / 365) | ||||
|     for months in range(max(estimated_months - 1, 0), estimated_months + 2): | ||||
|         proposed_next_month = add_months(billing_cycle_anchor, months) | ||||
|         if 20 < (proposed_next_month - dt).days < 40: | ||||
|             return proposed_next_month | ||||
|     raise AssertionError('Something wrong in next_month calculation with ' | ||||
|                          'billing_cycle_anchor: %s, dt: %s' % (billing_cycle_anchor, dt)) | ||||
|  | ||||
| # TODO take downgrade into account | ||||
| def next_renewal_date(plan: CustomerPlan, event_time: datetime) -> datetime: | ||||
|     months_per_period = { | ||||
|         CustomerPlan.ANNUAL: 12, | ||||
|         CustomerPlan.MONTHLY: 1, | ||||
|     }[plan.billing_schedule] | ||||
|     periods = 1 | ||||
|     dt = plan.billing_cycle_anchor | ||||
|     while dt <= event_time: | ||||
|         dt = add_months(plan.billing_cycle_anchor, months_per_period * periods) | ||||
|         periods += 1 | ||||
|     return dt | ||||
|  | ||||
| # TODO take downgrade into account | ||||
| def next_invoice_date(plan: CustomerPlan) -> datetime: | ||||
|     months_per_period = { | ||||
|         CustomerPlan.ANNUAL: 12, | ||||
|         CustomerPlan.MONTHLY: 1, | ||||
|     }[plan.billing_schedule] | ||||
|     if plan.automanage_licenses: | ||||
|         months_per_period = 1 | ||||
|     periods = 1 | ||||
|     dt = plan.billing_cycle_anchor | ||||
|     while dt <= plan.next_invoice_date: | ||||
|         dt = add_months(plan.billing_cycle_anchor, months_per_period * periods) | ||||
|         periods += 1 | ||||
|     return dt | ||||
|  | ||||
| def renewal_amount(plan: CustomerPlan, event_time: datetime) -> Optional[int]:  # nocoverage: TODO | ||||
|     if plan.fixed_price is not None: | ||||
|         return plan.fixed_price | ||||
|     last_ledger_entry = add_plan_renewal_to_license_ledger_if_needed(plan, event_time) | ||||
|     if last_ledger_entry.licenses_at_next_renewal is None: | ||||
|         return None | ||||
|     assert(plan.price_per_license is not None)  # for mypy | ||||
|     return plan.price_per_license * last_ledger_entry.licenses_at_next_renewal | ||||
|  | ||||
| class BillingError(Exception): | ||||
|     # error messages | ||||
|     CONTACT_SUPPORT = _("Something went wrong. Please contact %s." % (settings.ZULIP_ADMINISTRATOR,)) | ||||
|     TRY_RELOADING = _("Something went wrong. Please reload the page.") | ||||
|  | ||||
|     # description is used only for tests | ||||
|     def __init__(self, description: str, message: str=CONTACT_SUPPORT) -> None: | ||||
|     def __init__(self, description: str, message: str) -> None: | ||||
|         self.description = description | ||||
|         self.message = message | ||||
|  | ||||
| @@ -142,6 +89,9 @@ def catch_stripe_errors(func: CallableT) -> CallableT: | ||||
|             if STRIPE_PUBLISHABLE_KEY is None: | ||||
|                 raise BillingError('missing stripe config', "Missing Stripe config. " | ||||
|                                    "See https://zulip.readthedocs.io/en/latest/subsystems/billing.html.") | ||||
|             if not Plan.objects.exists(): | ||||
|                 raise BillingError('missing plans', | ||||
|                                    "Plan objects not created. Please run ./manage.py setup_stripe") | ||||
|         try: | ||||
|             return func(*args, **kwargs) | ||||
|         # See https://stripe.com/docs/api/python#error_handling, though | ||||
| @@ -164,11 +114,58 @@ def catch_stripe_errors(func: CallableT) -> CallableT: | ||||
|  | ||||
| @catch_stripe_errors | ||||
| def stripe_get_customer(stripe_customer_id: str) -> stripe.Customer: | ||||
|     return stripe.Customer.retrieve(stripe_customer_id, expand=["default_source"]) | ||||
|     stripe_customer = stripe.Customer.retrieve(stripe_customer_id, expand=["default_source"]) | ||||
|     if PRINT_STRIPE_FIXTURE_DATA: | ||||
|         print(''.join(['"customer_with_subscription": ', str(stripe_customer), ',']))  # nocoverage | ||||
|     return stripe_customer | ||||
|  | ||||
| @catch_stripe_errors | ||||
| def do_create_stripe_customer(user: UserProfile, stripe_token: Optional[str]=None) -> Customer: | ||||
| def stripe_get_upcoming_invoice(stripe_customer_id: str) -> stripe.Invoice: | ||||
|     stripe_invoice = stripe.Invoice.upcoming(customer=stripe_customer_id) | ||||
|     if PRINT_STRIPE_FIXTURE_DATA: | ||||
|         print(''.join(['"upcoming_invoice": ', str(stripe_invoice), ',']))  # nocoverage | ||||
|     return stripe_invoice | ||||
|  | ||||
| @catch_stripe_errors | ||||
| def stripe_get_invoice_preview_for_downgrade( | ||||
|         stripe_customer_id: str, stripe_subscription_id: str, | ||||
|         stripe_subscriptionitem_id: str) -> stripe.Invoice: | ||||
|     return stripe.Invoice.upcoming( | ||||
|         customer=stripe_customer_id, subscription=stripe_subscription_id, | ||||
|         subscription_items=[{'id': stripe_subscriptionitem_id, 'quantity': 0}]) | ||||
|  | ||||
| def preview_invoice_total_for_downgrade(stripe_customer: stripe.Customer) -> int: | ||||
|     stripe_subscription = extract_current_subscription(stripe_customer) | ||||
|     if stripe_subscription is None: | ||||
|         # Most likely situation is: user A goes to billing page, user B | ||||
|         # cancels subscription, user A clicks on "downgrade" or something | ||||
|         # else that calls this function. | ||||
|         billing_logger.error("Trying to extract subscription item that doesn't exist, for Stripe customer %s" | ||||
|                              % (stripe_customer.id,)) | ||||
|         raise BillingError('downgrade without subscription', BillingError.TRY_RELOADING) | ||||
|     for item in stripe_subscription['items']: | ||||
|         # There should only be one item, but we can't index into stripe_subscription['items'] | ||||
|         stripe_subscriptionitem_id = item.id | ||||
|     return stripe_get_invoice_preview_for_downgrade( | ||||
|         stripe_customer.id, stripe_subscription.id, stripe_subscriptionitem_id).total | ||||
|  | ||||
| # Return type should be Optional[stripe.Subscription], which throws a mypy error. | ||||
| # Will fix once we add type stubs for the Stripe API. | ||||
| def extract_current_subscription(stripe_customer: stripe.Customer) -> Any: | ||||
|     if not stripe_customer.subscriptions: | ||||
|         return None | ||||
|     for stripe_subscription in stripe_customer.subscriptions: | ||||
|         if stripe_subscription.status != "canceled": | ||||
|             return stripe_subscription | ||||
|     return None | ||||
|  | ||||
| @catch_stripe_errors | ||||
| def do_create_customer(user: UserProfile, stripe_token: Optional[str]=None, | ||||
|                        coupon: Optional[Coupon]=None) -> stripe.Customer: | ||||
|     realm = user.realm | ||||
|     stripe_coupon_id = None | ||||
|     if coupon is not None: | ||||
|         stripe_coupon_id = coupon.stripe_coupon_id | ||||
|     # We could do a better job of handling race conditions here, but if two | ||||
|     # people from a realm try to upgrade at exactly the same time, the main | ||||
|     # bad thing that will happen is that we will create an extra stripe | ||||
| @@ -177,7 +174,10 @@ def do_create_stripe_customer(user: UserProfile, stripe_token: Optional[str]=Non | ||||
|         description="%s (%s)" % (realm.string_id, realm.name), | ||||
|         email=user.email, | ||||
|         metadata={'realm_id': realm.id, 'realm_str': realm.string_id}, | ||||
|         source=stripe_token) | ||||
|         source=stripe_token, | ||||
|         coupon=stripe_coupon_id) | ||||
|     if PRINT_STRIPE_FIXTURE_DATA: | ||||
|         print(''.join(['"create_customer": ', str(stripe_customer), ',']))  # nocoverage | ||||
|     event_time = timestamp_to_datetime(stripe_customer.created) | ||||
|     with transaction.atomic(): | ||||
|         RealmAuditLog.objects.create( | ||||
| @@ -187,11 +187,10 @@ def do_create_stripe_customer(user: UserProfile, stripe_token: Optional[str]=Non | ||||
|             RealmAuditLog.objects.create( | ||||
|                 realm=user.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_CARD_CHANGED, | ||||
|                 event_time=event_time) | ||||
|         customer, created = Customer.objects.update_or_create(realm=realm, defaults={ | ||||
|             'stripe_customer_id': stripe_customer.id}) | ||||
|         Customer.objects.create(realm=realm, stripe_customer_id=stripe_customer.id) | ||||
|         user.is_billing_admin = True | ||||
|         user.save(update_fields=["is_billing_admin"]) | ||||
|     return customer | ||||
|     return stripe_customer | ||||
|  | ||||
| @catch_stripe_errors | ||||
| def do_replace_payment_source(user: UserProfile, stripe_token: str) -> stripe.Customer: | ||||
| @@ -206,262 +205,213 @@ def do_replace_payment_source(user: UserProfile, stripe_token: str) -> stripe.Cu | ||||
|         event_time=timezone_now()) | ||||
|     return updated_stripe_customer | ||||
|  | ||||
| # event_time should roughly be timezone_now(). Not designed to handle | ||||
| # event_times in the past or future | ||||
| # TODO handle downgrade | ||||
| def add_plan_renewal_to_license_ledger_if_needed(plan: CustomerPlan, event_time: datetime) -> LicenseLedger: | ||||
|     last_ledger_entry = LicenseLedger.objects.filter(plan=plan).order_by('-id').first() | ||||
|     last_renewal = LicenseLedger.objects.filter(plan=plan, is_renewal=True) \ | ||||
|                                         .order_by('-id').first().event_time | ||||
|     plan_renewal_date = next_renewal_date(plan, last_renewal) | ||||
|     if plan_renewal_date <= event_time: | ||||
|         return LicenseLedger.objects.create( | ||||
|             plan=plan, is_renewal=True, event_time=plan_renewal_date, | ||||
|             licenses=last_ledger_entry.licenses_at_next_renewal, | ||||
|             licenses_at_next_renewal=last_ledger_entry.licenses_at_next_renewal) | ||||
|     return last_ledger_entry | ||||
|  | ||||
| # Returns Customer instead of stripe_customer so that we don't make a Stripe | ||||
| # API call if there's nothing to update | ||||
| def update_or_create_stripe_customer(user: UserProfile, stripe_token: Optional[str]=None) -> Customer: | ||||
|     realm = user.realm | ||||
|     customer = Customer.objects.filter(realm=realm).first() | ||||
|     if customer is None or customer.stripe_customer_id is None: | ||||
|         return do_create_stripe_customer(user, stripe_token=stripe_token) | ||||
|     if stripe_token is not None: | ||||
|         do_replace_payment_source(user, stripe_token) | ||||
|     return customer | ||||
|  | ||||
| def compute_plan_parameters( | ||||
|         automanage_licenses: bool, billing_schedule: int, | ||||
|         discount: Optional[Decimal]) -> Tuple[datetime, datetime, datetime, int]: | ||||
|     # Everything in Stripe is stored as timestamps with 1 second resolution, | ||||
|     # so standardize on 1 second resolution. | ||||
|     # TODO talk about leapseconds? | ||||
|     billing_cycle_anchor = timezone_now().replace(microsecond=0) | ||||
|     if billing_schedule == CustomerPlan.ANNUAL: | ||||
|         # TODO use variables to account for Zulip Plus | ||||
|         price_per_license = 8000 | ||||
|         period_end = add_months(billing_cycle_anchor, 12) | ||||
|     elif billing_schedule == CustomerPlan.MONTHLY: | ||||
|         price_per_license = 800 | ||||
|         period_end = add_months(billing_cycle_anchor, 1) | ||||
|     else: | ||||
|         raise AssertionError('Unknown billing_schedule: {}'.format(billing_schedule)) | ||||
|     if discount is not None: | ||||
|         # There are no fractional cents in Stripe, so round down to nearest integer. | ||||
|         price_per_license = int(float(price_per_license * (1 - discount / 100)) + .00001) | ||||
|     next_invoice_date = period_end | ||||
|     if automanage_licenses: | ||||
|         next_invoice_date = add_months(billing_cycle_anchor, 1) | ||||
|     return billing_cycle_anchor, next_invoice_date, period_end, price_per_license | ||||
|  | ||||
| # Only used for cloud signups | ||||
| @catch_stripe_errors | ||||
| def process_initial_upgrade(user: UserProfile, licenses: int, automanage_licenses: bool, | ||||
|                             billing_schedule: int, stripe_token: Optional[str]) -> None: | ||||
|     realm = user.realm | ||||
|     customer = update_or_create_stripe_customer(user, stripe_token=stripe_token) | ||||
|     if CustomerPlan.objects.filter(customer=customer, status=CustomerPlan.ACTIVE).exists(): | ||||
|         # Unlikely race condition from two people upgrading (clicking "Make payment") | ||||
|         # at exactly the same time. Doesn't fully resolve the race condition, but having | ||||
|         # a check here reduces the likelihood. | ||||
|         billing_logger.warning( | ||||
|             "Customer {} trying to upgrade, but has an active subscription".format(customer)) | ||||
| def do_replace_coupon(user: UserProfile, coupon: Coupon) -> stripe.Customer: | ||||
|     stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) | ||||
|     stripe_customer.coupon = coupon.stripe_coupon_id | ||||
|     return stripe.Customer.save(stripe_customer) | ||||
|  | ||||
| @catch_stripe_errors | ||||
| def do_subscribe_customer_to_plan(user: UserProfile, stripe_customer: stripe.Customer, stripe_plan_id: str, | ||||
|                                   seat_count: int, tax_percent: float) -> None: | ||||
|     if extract_current_subscription(stripe_customer) is not None: | ||||
|         # Most likely due to two people in the org going to the billing page, | ||||
|         # and then both upgrading their plan. We don't send clients | ||||
|         # real-time event updates for the billing pages, so this is more | ||||
|         # likely than it would be in other parts of the app. | ||||
|         billing_logger.error("Stripe customer %s trying to subscribe to %s, " | ||||
|                              "but has an active subscription" % (stripe_customer.id, stripe_plan_id)) | ||||
|         raise BillingError('subscribing with existing subscription', BillingError.TRY_RELOADING) | ||||
|     customer = Customer.objects.get(stripe_customer_id=stripe_customer.id) | ||||
|     # Note that there is a race condition here, where if two users upgrade at exactly the | ||||
|     # same time, they will have two subscriptions, and get charged twice. We could try to | ||||
|     # reduce the chance of it with a well-designed idempotency_key, but it's not easy since | ||||
|     # we also need to be careful not to block the customer from retrying if their | ||||
|     # subscription attempt fails (e.g. due to insufficient funds). | ||||
|  | ||||
|     billing_cycle_anchor, next_invoice_date, period_end, price_per_license = compute_plan_parameters( | ||||
|         automanage_licenses, billing_schedule, customer.default_discount) | ||||
|     # The main design constraint in this function is that if you upgrade with a credit card, and the | ||||
|     # charge fails, everything should be rolled back as if nothing had happened. This is because we | ||||
|     # expect frequent card failures on initial signup. | ||||
|     # Hence, if we're going to charge a card, do it at the beginning, even if we later may have to | ||||
|     # adjust the number of licenses. | ||||
|     charge_automatically = stripe_token is not None | ||||
|     if charge_automatically: | ||||
|         stripe_charge = stripe.Charge.create( | ||||
|             amount=price_per_license * licenses, | ||||
|             currency='usd', | ||||
|             customer=customer.stripe_customer_id, | ||||
|             description="Upgrade to Zulip Standard, ${} x {}".format(price_per_license/100, licenses), | ||||
|             receipt_email=user.email, | ||||
|             statement_descriptor='Zulip Standard') | ||||
|         # Not setting a period start and end, but maybe we should? Unclear what will make things | ||||
|         # most similar to the renewal case from an accounting perspective. | ||||
|         stripe.InvoiceItem.create( | ||||
|             amount=price_per_license * licenses * -1, | ||||
|             currency='usd', | ||||
|             customer=customer.stripe_customer_id, | ||||
|             description="Payment (Card ending in {})".format(cast(stripe.Card, stripe_charge.source).last4), | ||||
|             discountable=False) | ||||
|  | ||||
|     # TODO: The correctness of this relies on user creation, deactivation, etc being | ||||
|     # in a transaction.atomic() with the relevant RealmAuditLog entries | ||||
|     # Success here implies the stripe_customer was charged: https://stripe.com/docs/billing/lifecycle#active | ||||
|     # Otherwise we should expect it to throw a stripe.error. | ||||
|     stripe_subscription = stripe.Subscription.create( | ||||
|         customer=stripe_customer.id, | ||||
|         billing='charge_automatically', | ||||
|         items=[{ | ||||
|             'plan': stripe_plan_id, | ||||
|             'quantity': seat_count, | ||||
|         }], | ||||
|         prorate=True, | ||||
|         tax_percent=tax_percent) | ||||
|     if PRINT_STRIPE_FIXTURE_DATA: | ||||
|         print(''.join(['"create_subscription": ', str(stripe_subscription), ',']))  # nocoverage | ||||
|     with transaction.atomic(): | ||||
|         # billed_licenses can greater than licenses if users are added between the start of | ||||
|         # this function (process_initial_upgrade) and now | ||||
|         billed_licenses = max(get_seat_count(realm), licenses) | ||||
|         plan_params = { | ||||
|             'automanage_licenses': automanage_licenses, | ||||
|             'charge_automatically': charge_automatically, | ||||
|             'price_per_license': price_per_license, | ||||
|             'discount': customer.default_discount, | ||||
|             'billing_cycle_anchor': billing_cycle_anchor, | ||||
|             'billing_schedule': billing_schedule, | ||||
|             'tier': CustomerPlan.STANDARD} | ||||
|         plan = CustomerPlan.objects.create( | ||||
|             customer=customer, | ||||
|             next_invoice_date=next_invoice_date, | ||||
|             **plan_params) | ||||
|         ledger_entry = LicenseLedger.objects.create( | ||||
|             plan=plan, | ||||
|             is_renewal=True, | ||||
|             event_time=billing_cycle_anchor, | ||||
|             licenses=billed_licenses, | ||||
|             licenses_at_next_renewal=billed_licenses) | ||||
|         plan.invoiced_through = ledger_entry | ||||
|         plan.save(update_fields=['invoiced_through']) | ||||
|         customer.has_billing_relationship = True | ||||
|         customer.save(update_fields=['has_billing_relationship']) | ||||
|         customer.realm.has_seat_based_plan = True | ||||
|         customer.realm.save(update_fields=['has_seat_based_plan']) | ||||
|         RealmAuditLog.objects.create( | ||||
|             realm=realm, acting_user=user, event_time=billing_cycle_anchor, | ||||
|             event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED, | ||||
|             extra_data=ujson.dumps(plan_params)) | ||||
|     stripe.InvoiceItem.create( | ||||
|         currency='usd', | ||||
|         customer=customer.stripe_customer_id, | ||||
|         description='Zulip Standard', | ||||
|         discountable=False, | ||||
|         period = {'start': datetime_to_timestamp(billing_cycle_anchor), | ||||
|                   'end': datetime_to_timestamp(period_end)}, | ||||
|         quantity=billed_licenses, | ||||
|         unit_amount=price_per_license) | ||||
|             realm=customer.realm, | ||||
|             acting_user=user, | ||||
|             event_type=RealmAuditLog.STRIPE_PLAN_CHANGED, | ||||
|             event_time=timestamp_to_datetime(stripe_subscription.created), | ||||
|             extra_data=ujson.dumps({'plan': stripe_plan_id, 'quantity': seat_count})) | ||||
|  | ||||
|     if charge_automatically: | ||||
|         billing_method = 'charge_automatically' | ||||
|         days_until_due = None | ||||
|     else: | ||||
|         billing_method = 'send_invoice' | ||||
|         days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE | ||||
|     stripe_invoice = stripe.Invoice.create( | ||||
|         auto_advance=True, | ||||
|         billing=billing_method, | ||||
|         customer=customer.stripe_customer_id, | ||||
|         days_until_due=days_until_due, | ||||
|         statement_descriptor='Zulip Standard') | ||||
|     stripe.Invoice.finalize_invoice(stripe_invoice) | ||||
|         current_seat_count = get_seat_count(customer.realm) | ||||
|         if seat_count != current_seat_count: | ||||
|             RealmAuditLog.objects.create( | ||||
|                 realm=customer.realm, | ||||
|                 event_type=RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET, | ||||
|                 event_time=timestamp_to_datetime(stripe_subscription.created), | ||||
|                 requires_billing_update=True, | ||||
|                 extra_data=ujson.dumps({'quantity': current_seat_count})) | ||||
|  | ||||
|     from zerver.lib.actions import do_change_plan_type | ||||
|     do_change_plan_type(realm, Realm.STANDARD) | ||||
|  | ||||
| def update_license_ledger_for_automanaged_plan(realm: Realm, plan: CustomerPlan, | ||||
|                                                event_time: datetime) -> None: | ||||
|     last_ledger_entry = add_plan_renewal_to_license_ledger_if_needed(plan, event_time) | ||||
|     # todo: handle downgrade, where licenses_at_next_renewal should be 0 | ||||
|     licenses_at_next_renewal = get_seat_count(realm) | ||||
|     licenses = max(licenses_at_next_renewal, last_ledger_entry.licenses) | ||||
|     LicenseLedger.objects.create( | ||||
|         plan=plan, event_time=event_time, licenses=licenses, | ||||
|         licenses_at_next_renewal=licenses_at_next_renewal) | ||||
|  | ||||
| def update_license_ledger_if_needed(realm: Realm, event_time: datetime) -> None: | ||||
|     customer = Customer.objects.filter(realm=realm).first() | ||||
| def process_initial_upgrade(user: UserProfile, plan: Plan, seat_count: int, stripe_token: str) -> None: | ||||
|     customer = Customer.objects.filter(realm=user.realm).first() | ||||
|     if customer is None: | ||||
|         return | ||||
|     plan = get_active_plan(customer) | ||||
|     if plan is None: | ||||
|         return | ||||
|     if not plan.automanage_licenses: | ||||
|         return | ||||
|     update_license_ledger_for_automanaged_plan(realm, plan, event_time) | ||||
|         stripe_customer = do_create_customer(user, stripe_token=stripe_token) | ||||
|     else: | ||||
|         stripe_customer = do_replace_payment_source(user, stripe_token) | ||||
|     do_subscribe_customer_to_plan( | ||||
|         user=user, | ||||
|         stripe_customer=stripe_customer, | ||||
|         stripe_plan_id=plan.stripe_plan_id, | ||||
|         seat_count=seat_count, | ||||
|         # TODO: billing address details are passed to us in the request; | ||||
|         # use that to calculate taxes. | ||||
|         tax_percent=0) | ||||
|     do_change_plan_type(user, Realm.STANDARD) | ||||
|  | ||||
| def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None: | ||||
|     if plan.invoicing_status == CustomerPlan.STARTED: | ||||
|         raise NotImplementedError('Plan with invoicing_status==STARTED needs manual resolution.') | ||||
|     add_plan_renewal_to_license_ledger_if_needed(plan, event_time) | ||||
|     assert(plan.invoiced_through is not None) | ||||
|     licenses_base = plan.invoiced_through.licenses | ||||
|     invoice_item_created = False | ||||
|     for ledger_entry in LicenseLedger.objects.filter(plan=plan, id__gt=plan.invoiced_through.id, | ||||
|                                                      event_time__lte=event_time).order_by('id'): | ||||
|         price_args = {}  # type: Dict[str, int] | ||||
|         if ledger_entry.is_renewal: | ||||
|             if plan.fixed_price is not None: | ||||
|                 price_args = {'amount': plan.fixed_price} | ||||
| def attach_discount_to_realm(user: UserProfile, percent_off: int) -> None: | ||||
|     coupon = Coupon.objects.get(percent_off=percent_off) | ||||
|     customer = Customer.objects.filter(realm=user.realm).first() | ||||
|     if customer is None: | ||||
|         do_create_customer(user, coupon=coupon) | ||||
|     else: | ||||
|         do_replace_coupon(user, coupon) | ||||
|  | ||||
| @catch_stripe_errors | ||||
| def process_downgrade(user: UserProfile) -> None: | ||||
|     stripe_customer = stripe_get_customer( | ||||
|         Customer.objects.filter(realm=user.realm).first().stripe_customer_id) | ||||
|     subscription_balance = preview_invoice_total_for_downgrade(stripe_customer) | ||||
|     # If subscription_balance > 0, they owe us money. This is likely due to | ||||
|     # people they added in the last day, so we can just forgive it. | ||||
|     # Stripe automatically forgives it when we delete the subscription, so nothing we need to do there. | ||||
|     if subscription_balance < 0: | ||||
|         stripe_customer.account_balance = stripe_customer.account_balance + subscription_balance | ||||
|     stripe_subscription = extract_current_subscription(stripe_customer) | ||||
|     # Wish these two could be transaction.atomic | ||||
|     stripe_subscription = stripe_subscription.delete() | ||||
|     stripe.Customer.save(stripe_customer) | ||||
|     with transaction.atomic(): | ||||
|         user.realm.has_seat_based_plan = False | ||||
|         user.realm.save(update_fields=['has_seat_based_plan']) | ||||
|         RealmAuditLog.objects.create( | ||||
|             realm=user.realm, | ||||
|             acting_user=user, | ||||
|             event_type=RealmAuditLog.STRIPE_PLAN_CHANGED, | ||||
|             event_time=timestamp_to_datetime(stripe_subscription.canceled_at), | ||||
|             extra_data=ujson.dumps({'plan': None, 'quantity': stripe_subscription.quantity})) | ||||
|     # Doing this last, since it results in user-visible confirmation (via | ||||
|     # product changes) that the downgrade succeeded. | ||||
|     # Keeping it out of the transaction.atomic block because it will | ||||
|     # eventually have a lot of stuff going on. | ||||
|     do_change_plan_type(user, Realm.LIMITED) | ||||
|  | ||||
| ## Process RealmAuditLog | ||||
|  | ||||
| def do_set_subscription_quantity( | ||||
|         customer: Customer, timestamp: int, idempotency_key: str, quantity: int) -> None: | ||||
|     stripe_customer = stripe_get_customer(customer.stripe_customer_id) | ||||
|     stripe_subscription = extract_current_subscription(stripe_customer) | ||||
|     stripe_subscription.quantity = quantity | ||||
|     stripe_subscription.proration_date = timestamp | ||||
|     stripe_subscription.save(idempotency_key=idempotency_key) | ||||
|  | ||||
| def do_adjust_subscription_quantity( | ||||
|         customer: Customer, timestamp: int, idempotency_key: str, delta: int) -> None: | ||||
|     stripe_customer = stripe_get_customer(customer.stripe_customer_id) | ||||
|     stripe_subscription = extract_current_subscription(stripe_customer) | ||||
|     stripe_subscription.quantity = stripe_subscription.quantity + delta | ||||
|     stripe_subscription.proration_date = timestamp | ||||
|     stripe_subscription.save(idempotency_key=idempotency_key) | ||||
|  | ||||
| def increment_subscription_quantity( | ||||
|         customer: Customer, timestamp: int, idempotency_key: str) -> None: | ||||
|     return do_adjust_subscription_quantity(customer, timestamp, idempotency_key, 1) | ||||
|  | ||||
| def decrement_subscription_quantity( | ||||
|         customer: Customer, timestamp: int, idempotency_key: str) -> None: | ||||
|     return do_adjust_subscription_quantity(customer, timestamp, idempotency_key, -1) | ||||
|  | ||||
| @catch_stripe_errors | ||||
| def process_billing_log_entry(processor: BillingProcessor, log_row: RealmAuditLog) -> None: | ||||
|     processor.state = BillingProcessor.STARTED | ||||
|     processor.log_row = log_row | ||||
|     processor.save() | ||||
|  | ||||
|     customer = Customer.objects.get(realm=log_row.realm) | ||||
|     timestamp = datetime_to_timestamp(log_row.event_time) | ||||
|     idempotency_key = 'process_billing_log_entry:%s' % (log_row.id,) | ||||
|     extra_args = {}  # type: Dict[str, Any] | ||||
|     if log_row.extra_data is not None: | ||||
|         extra_args = ujson.loads(log_row.extra_data) | ||||
|     processing_functions = { | ||||
|         RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET: do_set_subscription_quantity, | ||||
|         RealmAuditLog.USER_CREATED: increment_subscription_quantity, | ||||
|         RealmAuditLog.USER_ACTIVATED: increment_subscription_quantity, | ||||
|         RealmAuditLog.USER_DEACTIVATED: decrement_subscription_quantity, | ||||
|         RealmAuditLog.USER_REACTIVATED: increment_subscription_quantity, | ||||
|     }  # type: Dict[str, Callable[..., None]] | ||||
|     processing_functions[log_row.event_type](customer, timestamp, idempotency_key, **extra_args) | ||||
|  | ||||
|     processor.state = BillingProcessor.DONE | ||||
|     processor.save() | ||||
|  | ||||
| def get_next_billing_log_entry(processor: BillingProcessor) -> Optional[RealmAuditLog]: | ||||
|     if processor.state == BillingProcessor.STARTED: | ||||
|         return processor.log_row | ||||
|     assert processor.state != BillingProcessor.STALLED | ||||
|     if processor.state not in [BillingProcessor.DONE, BillingProcessor.SKIPPED]: | ||||
|         raise BillingError( | ||||
|             'unknown processor state', | ||||
|             "Check for typos, since this value is sometimes set by hand: %s" % (processor.state,)) | ||||
|  | ||||
|     if processor.realm is None: | ||||
|         realms_with_processors = BillingProcessor.objects.exclude( | ||||
|             realm=None).values_list('realm', flat=True) | ||||
|         query = RealmAuditLog.objects.exclude(realm__in=realms_with_processors) | ||||
|     else: | ||||
|         global_processor = BillingProcessor.objects.get(realm=None) | ||||
|         query = RealmAuditLog.objects.filter( | ||||
|             realm=processor.realm, id__lt=global_processor.log_row.id) | ||||
|     return query.filter(id__gt=processor.log_row.id, | ||||
|                         requires_billing_update=True).order_by('id').first() | ||||
|  | ||||
| def run_billing_processor_one_step(processor: BillingProcessor) -> bool: | ||||
|     # Returns True if a row was processed, or if processing was attempted | ||||
|     log_row = get_next_billing_log_entry(processor) | ||||
|     if log_row is None: | ||||
|         if processor.realm is not None: | ||||
|             processor.delete() | ||||
|         return False | ||||
|     try: | ||||
|         process_billing_log_entry(processor, log_row) | ||||
|         return True | ||||
|     except Exception as e: | ||||
|         # Possible errors include processing subscription quantity entries | ||||
|         # after downgrade, since the downgrade code doesn't check that | ||||
|         # billing processor is up to date | ||||
|         billing_logger.error("Error on log_row.realm=%s, event_type=%s, log_row.id=%s, " | ||||
|                              "processor.id=%s, processor.realm=%s" % ( | ||||
|                                  processor.log_row.realm.string_id, processor.log_row.event_type, | ||||
|                                  processor.log_row.id, processor.id, processor.realm)) | ||||
|         if isinstance(e, StripeCardError): | ||||
|             if processor.realm is None: | ||||
|                 BillingProcessor.objects.create(log_row=processor.log_row, | ||||
|                                                 realm=processor.log_row.realm, | ||||
|                                                 state=BillingProcessor.STALLED) | ||||
|                 processor.state = BillingProcessor.SKIPPED | ||||
|             else: | ||||
|                 assert(plan.price_per_license is not None)  # needed for mypy | ||||
|                 price_args = {'unit_amount': plan.price_per_license, | ||||
|                               'quantity': ledger_entry.licenses} | ||||
|             description = "Zulip Standard - renewal" | ||||
|         elif ledger_entry.licenses != licenses_base: | ||||
|             assert(plan.price_per_license) | ||||
|             last_renewal = LicenseLedger.objects.filter( | ||||
|                 plan=plan, is_renewal=True, event_time__lte=ledger_entry.event_time) \ | ||||
|                 .order_by('-id').first().event_time | ||||
|             period_end = next_renewal_date(plan, ledger_entry.event_time) | ||||
|             proration_fraction = (period_end - ledger_entry.event_time) / (period_end - last_renewal) | ||||
|             price_args = {'unit_amount': int(plan.price_per_license * proration_fraction + .5), | ||||
|                           'quantity': ledger_entry.licenses - licenses_base} | ||||
|             description = "Additional license ({} - {})".format( | ||||
|                 ledger_entry.event_time.strftime('%b %-d, %Y'), period_end.strftime('%b %-d, %Y')) | ||||
|  | ||||
|         if price_args: | ||||
|             plan.invoiced_through = ledger_entry | ||||
|             plan.invoicing_status = CustomerPlan.STARTED | ||||
|             plan.save(update_fields=['invoicing_status', 'invoiced_through']) | ||||
|             idempotency_key = 'ledger_entry:{}'.format(ledger_entry.id)  # type: Optional[str] | ||||
|             if settings.TEST_SUITE: | ||||
|                 idempotency_key = None | ||||
|             stripe.InvoiceItem.create( | ||||
|                 currency='usd', | ||||
|                 customer=plan.customer.stripe_customer_id, | ||||
|                 description=description, | ||||
|                 discountable=False, | ||||
|                 period = {'start': datetime_to_timestamp(ledger_entry.event_time), | ||||
|                           'end': datetime_to_timestamp(next_renewal_date(plan, ledger_entry.event_time))}, | ||||
|                 idempotency_key=idempotency_key, | ||||
|                 **price_args) | ||||
|             invoice_item_created = True | ||||
|         plan.invoiced_through = ledger_entry | ||||
|         plan.invoicing_status = CustomerPlan.DONE | ||||
|         plan.save(update_fields=['invoicing_status', 'invoiced_through']) | ||||
|         licenses_base = ledger_entry.licenses | ||||
|  | ||||
|     if invoice_item_created: | ||||
|         if plan.charge_automatically: | ||||
|             billing_method = 'charge_automatically' | ||||
|             days_until_due = None | ||||
|         else: | ||||
|             billing_method = 'send_invoice' | ||||
|             days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE | ||||
|         stripe_invoice = stripe.Invoice.create( | ||||
|             auto_advance=True, | ||||
|             billing=billing_method, | ||||
|             customer=plan.customer.stripe_customer_id, | ||||
|             days_until_due=days_until_due, | ||||
|             statement_descriptor='Zulip Standard') | ||||
|         stripe.Invoice.finalize_invoice(stripe_invoice) | ||||
|  | ||||
|     plan.next_invoice_date = next_invoice_date(plan) | ||||
|     plan.save(update_fields=['next_invoice_date']) | ||||
|  | ||||
| def invoice_plans_as_needed(event_time: datetime) -> None: | ||||
|     for plan in CustomerPlan.objects.filter(next_invoice_date__lte=event_time): | ||||
|         invoice_plan(plan, event_time) | ||||
|  | ||||
| def attach_discount_to_realm(realm: Realm, discount: Decimal) -> None: | ||||
|     Customer.objects.update_or_create(realm=realm, defaults={'default_discount': discount}) | ||||
|  | ||||
| def process_downgrade(user: UserProfile) -> None:  # nocoverage | ||||
|     pass | ||||
|  | ||||
| def estimate_annual_recurring_revenue_by_realm() -> Dict[str, int]:  # nocoverage | ||||
|     annual_revenue = {} | ||||
|     for plan in CustomerPlan.objects.filter( | ||||
|             status=CustomerPlan.ACTIVE).select_related('customer__realm'): | ||||
|         # TODO: figure out what to do for plans that don't automatically | ||||
|         # renew, but which probably will renew | ||||
|         renewal_cents = renewal_amount(plan, timezone_now()) or 0 | ||||
|         if plan.billing_schedule == CustomerPlan.MONTHLY: | ||||
|             renewal_cents *= 12 | ||||
|         # TODO: Decimal stuff | ||||
|         annual_revenue[plan.customer.realm.string_id] = int(renewal_cents / 100) | ||||
|     return annual_revenue | ||||
|                 processor.state = BillingProcessor.STALLED | ||||
|             processor.save() | ||||
|             return True | ||||
|         raise | ||||
|   | ||||
							
								
								
									
										47
									
								
								corporate/management/commands/process_billing_updates.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								corporate/management/commands/process_billing_updates.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| """\ | ||||
| Run BillingProcessors. | ||||
|  | ||||
| This management command is run via supervisor. Do not run on multiple | ||||
| machines, as the code has not been made robust to race conditions from doing | ||||
| so.  (Alternatively, you can set `BILLING_PROCESSOR_ENABLED=False` on all but | ||||
| one machine to make the command have no effect.) | ||||
| """ | ||||
|  | ||||
| import time | ||||
| from typing import Any | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from zerver.lib.context_managers import lockfile | ||||
| from zerver.lib.management import sleep_forever | ||||
| from corporate.lib.stripe import StripeConnectionError, \ | ||||
|     run_billing_processor_one_step | ||||
| from corporate.models import BillingProcessor | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = """Run BillingProcessors, to sync billing-relevant updates into Stripe. | ||||
|  | ||||
| Run this command under supervisor. | ||||
|  | ||||
| Usage: ./manage.py process_billing_updates | ||||
| """ | ||||
|  | ||||
|     def handle(self, *args: Any, **options: Any) -> None: | ||||
|         if not settings.BILLING_PROCESSOR_ENABLED: | ||||
|             sleep_forever() | ||||
|  | ||||
|         with lockfile("/tmp/zulip_billing_processor.lockfile"): | ||||
|             while True: | ||||
|                 for processor in BillingProcessor.objects.exclude( | ||||
|                         state=BillingProcessor.STALLED): | ||||
|                     try: | ||||
|                         entry_processed = run_billing_processor_one_step(processor) | ||||
|                     except StripeConnectionError: | ||||
|                         time.sleep(5*60) | ||||
|                     # Less load on the db during times of activity | ||||
|                     # and more responsiveness when the load is low | ||||
|                     if entry_processed: | ||||
|                         time.sleep(10) | ||||
|                     else: | ||||
|                         time.sleep(2) | ||||
							
								
								
									
										55
									
								
								corporate/management/commands/setup_stripe.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								corporate/management/commands/setup_stripe.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| from zerver.lib.management import ZulipBaseCommand | ||||
| from corporate.models import Plan, Coupon, Customer | ||||
| from zproject.settings import get_secret | ||||
|  | ||||
| from typing import Any | ||||
|  | ||||
| import stripe | ||||
| stripe.api_key = get_secret('stripe_secret_key') | ||||
|  | ||||
| class Command(ZulipBaseCommand): | ||||
|     help = """Script to add the appropriate products and plans to Stripe.""" | ||||
|  | ||||
|     def handle(self, *args: Any, **options: Any) -> None: | ||||
|         Customer.objects.all().delete() | ||||
|         Plan.objects.all().delete() | ||||
|         Coupon.objects.all().delete() | ||||
|  | ||||
|         # Zulip Cloud offerings | ||||
|         product = stripe.Product.create( | ||||
|             name="Zulip Cloud Standard", | ||||
|             type='service', | ||||
|             statement_descriptor="Zulip Cloud Standard", | ||||
|             unit_label="user") | ||||
|  | ||||
|         plan = stripe.Plan.create( | ||||
|             currency='usd', | ||||
|             interval='month', | ||||
|             product=product.id, | ||||
|             amount=800, | ||||
|             billing_scheme='per_unit', | ||||
|             nickname=Plan.CLOUD_MONTHLY, | ||||
|             usage_type='licensed') | ||||
|         Plan.objects.create(nickname=Plan.CLOUD_MONTHLY, stripe_plan_id=plan.id) | ||||
|  | ||||
|         plan = stripe.Plan.create( | ||||
|             currency='usd', | ||||
|             interval='year', | ||||
|             product=product.id, | ||||
|             amount=8000, | ||||
|             billing_scheme='per_unit', | ||||
|             nickname=Plan.CLOUD_ANNUAL, | ||||
|             usage_type='licensed') | ||||
|         Plan.objects.create(nickname=Plan.CLOUD_ANNUAL, stripe_plan_id=plan.id) | ||||
|  | ||||
|         coupon = stripe.Coupon.create( | ||||
|             duration='forever', | ||||
|             name='25% discount', | ||||
|             percent_off=25) | ||||
|         Coupon.objects.create(percent_off=25, stripe_coupon_id=coupon.id) | ||||
|  | ||||
|         coupon = stripe.Coupon.create( | ||||
|             duration='forever', | ||||
|             name='85% discount', | ||||
|             percent_off=85) | ||||
|         Coupon.objects.create(percent_off=85, stripe_coupon_id=coupon.id) | ||||
| @@ -1,20 +0,0 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.11.16 on 2018-12-12 20:19 | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('corporate', '0001_initial'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='customer', | ||||
|             name='default_discount', | ||||
|             field=models.DecimalField(decimal_places=4, max_digits=7, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,35 +0,0 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.11.16 on 2018-12-22 21:05 | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('corporate', '0002_customer_default_discount'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='CustomerPlan', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('licenses', models.IntegerField()), | ||||
|                 ('automanage_licenses', models.BooleanField(default=False)), | ||||
|                 ('charge_automatically', models.BooleanField(default=False)), | ||||
|                 ('price_per_license', models.IntegerField(null=True)), | ||||
|                 ('fixed_price', models.IntegerField(null=True)), | ||||
|                 ('discount', models.DecimalField(decimal_places=4, max_digits=6, null=True)), | ||||
|                 ('billing_cycle_anchor', models.DateTimeField()), | ||||
|                 ('billing_schedule', models.SmallIntegerField()), | ||||
|                 ('billed_through', models.DateTimeField()), | ||||
|                 ('next_billing_date', models.DateTimeField(db_index=True)), | ||||
|                 ('tier', models.SmallIntegerField()), | ||||
|                 ('status', models.SmallIntegerField(default=1)), | ||||
|                 ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='corporate.Customer')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,27 +0,0 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.11.18 on 2019-01-19 05:01 | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('corporate', '0003_customerplan'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='LicenseLedger', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('is_renewal', models.BooleanField(default=False)), | ||||
|                 ('event_time', models.DateTimeField()), | ||||
|                 ('licenses', models.IntegerField()), | ||||
|                 ('licenses_at_next_renewal', models.IntegerField(null=True)), | ||||
|                 ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='corporate.CustomerPlan')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,35 +0,0 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.11.18 on 2019-01-28 13:04 | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('corporate', '0004_licenseledger'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RenameField( | ||||
|             model_name='customerplan', | ||||
|             old_name='next_billing_date', | ||||
|             new_name='next_invoice_date', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='customerplan', | ||||
|             name='billed_through', | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='customerplan', | ||||
|             name='invoiced_through', | ||||
|             field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='corporate.LicenseLedger'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='customerplan', | ||||
|             name='invoicing_status', | ||||
|             field=models.SmallIntegerField(default=1), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,20 +0,0 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.11.18 on 2019-01-29 01:46 | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('corporate', '0005_customerplan_invoicing'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='customer', | ||||
|             name='stripe_customer_id', | ||||
|             field=models.CharField(max_length=255, null=True, unique=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,40 +0,0 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.11.18 on 2019-01-31 22:16 | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('corporate', '0006_nullable_stripe_customer_id'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name='billingprocessor', | ||||
|             name='log_row', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='billingprocessor', | ||||
|             name='realm', | ||||
|         ), | ||||
|         migrations.DeleteModel( | ||||
|             name='Coupon', | ||||
|         ), | ||||
|         migrations.DeleteModel( | ||||
|             name='Plan', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='customer', | ||||
|             name='has_billing_relationship', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='customerplan', | ||||
|             name='licenses', | ||||
|         ), | ||||
|         migrations.DeleteModel( | ||||
|             name='BillingProcessor', | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,70 +1,46 @@ | ||||
| import datetime | ||||
| from decimal import Decimal | ||||
| from typing import Optional | ||||
|  | ||||
| from django.db import models | ||||
| from django.db.models import CASCADE | ||||
|  | ||||
| from zerver.models import Realm | ||||
| from zerver.models import Realm, RealmAuditLog | ||||
|  | ||||
| class Customer(models.Model): | ||||
|     realm = models.OneToOneField(Realm, on_delete=CASCADE)  # type: Realm | ||||
|     stripe_customer_id = models.CharField(max_length=255, null=True, unique=True)  # type: str | ||||
|     # A percentage, like 85. | ||||
|     default_discount = models.DecimalField(decimal_places=4, max_digits=7, null=True)  # type: Optional[Decimal] | ||||
|     realm = models.OneToOneField(Realm, on_delete=models.CASCADE)  # type: Realm | ||||
|     stripe_customer_id = models.CharField(max_length=255, unique=True)  # type: str | ||||
|     # Becomes True the first time a payment successfully goes through, and never | ||||
|     # goes back to being False | ||||
|     has_billing_relationship = models.BooleanField(default=False)  # type: bool | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return "<Customer %s %s>" % (self.realm, self.stripe_customer_id) | ||||
|  | ||||
| class CustomerPlan(models.Model): | ||||
|     customer = models.ForeignKey(Customer, on_delete=CASCADE)  # type: Customer | ||||
|     automanage_licenses = models.BooleanField(default=False)  # type: bool | ||||
|     charge_automatically = models.BooleanField(default=False)  # type: bool | ||||
| class Plan(models.Model): | ||||
|     # The two possible values for nickname | ||||
|     CLOUD_MONTHLY = 'monthly' | ||||
|     CLOUD_ANNUAL = 'annual' | ||||
|     nickname = models.CharField(max_length=40, unique=True)  # type: str | ||||
|  | ||||
|     # Both of these are in cents. Exactly one of price_per_license or | ||||
|     # fixed_price should be set. fixed_price is only for manual deals, and | ||||
|     # can't be set via the self-serve billing system. | ||||
|     price_per_license = models.IntegerField(null=True)  # type: Optional[int] | ||||
|     fixed_price = models.IntegerField(null=True)  # type: Optional[int] | ||||
|     stripe_plan_id = models.CharField(max_length=255, unique=True)  # type: str | ||||
|  | ||||
|     # Discount that was applied. For display purposes only. | ||||
|     discount = models.DecimalField(decimal_places=4, max_digits=6, null=True)  # type: Optional[Decimal] | ||||
| class Coupon(models.Model): | ||||
|     percent_off = models.SmallIntegerField(unique=True)  # type: int | ||||
|     stripe_coupon_id = models.CharField(max_length=255, unique=True)  # type: str | ||||
|  | ||||
|     billing_cycle_anchor = models.DateTimeField()  # type: datetime.datetime | ||||
|     ANNUAL = 1 | ||||
|     MONTHLY = 2 | ||||
|     billing_schedule = models.SmallIntegerField()  # type: int | ||||
|     def __str__(self) -> str: | ||||
|         return '<Coupon: %s %s %s>' % (self.percent_off, self.stripe_coupon_id, self.id) | ||||
|  | ||||
|     next_invoice_date = models.DateTimeField(db_index=True)  # type: datetime.datetime | ||||
|     invoiced_through = models.ForeignKey( | ||||
|         'LicenseLedger', null=True, on_delete=CASCADE, related_name='+')  # type: Optional[LicenseLedger] | ||||
|     DONE = 1 | ||||
|     STARTED = 2 | ||||
|     invoicing_status = models.SmallIntegerField(default=DONE)  # type: int | ||||
| class BillingProcessor(models.Model): | ||||
|     log_row = models.ForeignKey(RealmAuditLog, on_delete=models.CASCADE)  # RealmAuditLog | ||||
|     # Exactly one processor, the global processor, has realm=None. | ||||
|     realm = models.OneToOneField(Realm, null=True, on_delete=models.CASCADE)  # type: Realm | ||||
|  | ||||
|     STANDARD = 1 | ||||
|     PLUS = 2  # not available through self-serve signup | ||||
|     ENTERPRISE = 10 | ||||
|     tier = models.SmallIntegerField()  # type: int | ||||
|     DONE = 'done' | ||||
|     STARTED = 'started' | ||||
|     SKIPPED = 'skipped'  # global processor only | ||||
|     STALLED = 'stalled'  # realm processors only | ||||
|     state = models.CharField(max_length=20)  # type: str | ||||
|  | ||||
|     ACTIVE = 1 | ||||
|     ENDED = 2 | ||||
|     NEVER_STARTED = 3 | ||||
|     # You can only have 1 active subscription at a time | ||||
|     status = models.SmallIntegerField(default=ACTIVE)  # type: int | ||||
|     last_modified = models.DateTimeField(auto_now=True)  # type: datetime.datetime | ||||
|  | ||||
|     # TODO maybe override setattr to ensure billing_cycle_anchor, etc are immutable | ||||
|  | ||||
| def get_active_plan(customer: Customer) -> Optional[CustomerPlan]: | ||||
|     return CustomerPlan.objects.filter(customer=customer, status=CustomerPlan.ACTIVE).first() | ||||
|  | ||||
| class LicenseLedger(models.Model): | ||||
|     plan = models.ForeignKey(CustomerPlan, on_delete=CASCADE)  # type: CustomerPlan | ||||
|     # Also True for the initial upgrade. | ||||
|     is_renewal = models.BooleanField(default=False)  # type: bool | ||||
|     event_time = models.DateTimeField()  # type: datetime.datetime | ||||
|     licenses = models.IntegerField()  # type: int | ||||
|     # None means the plan does not automatically renew. | ||||
|     # 0 means the plan has been explicitly downgraded. | ||||
|     # This cannot be None if plan.automanage_licenses. | ||||
|     licenses_at_next_renewal = models.IntegerField(null=True)  # type: Optional[int] | ||||
|     def __str__(self) -> str: | ||||
|         return '<BillingProcessor: %s %s %s>' % (self.realm, self.log_row, self.id) | ||||
|   | ||||
							
								
								
									
										411
									
								
								corporate/tests/stripe_fixtures.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										411
									
								
								corporate/tests/stripe_fixtures.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,411 @@ | ||||
| { | ||||
|     "create_customer": { | ||||
|         "account_balance": 0, | ||||
|         "created": 1529990750, | ||||
|         "currency": null, | ||||
|         "default_source": "card_1Ch9gVGh0CmXqmnwv94RombT", | ||||
|         "delinquent": false, | ||||
|         "description": "zulip (Zulip Dev)", | ||||
|         "discount": { | ||||
|             "coupon": { | ||||
|                 "amount_off": null, | ||||
|                 "created": 1535002820, | ||||
|                 "currency": null, | ||||
|                 "duration": "forever", | ||||
|                 "duration_in_months": null, | ||||
|                 "id": "rncBblSZ", | ||||
|                 "livemode": false, | ||||
|                 "max_redemptions": null, | ||||
|                 "metadata": {}, | ||||
|                 "name": "85% discount", | ||||
|                 "object": "coupon", | ||||
|                 "percent_off": 85.0, | ||||
|                 "redeem_by": null, | ||||
|                 "times_redeemed": 1, | ||||
|                 "valid": true | ||||
|             }, | ||||
|             "customer": "cus_DT7pd3yW0w8lF1", | ||||
|             "end": null, | ||||
|             "object": "discount", | ||||
|             "start": 1535004909, | ||||
|             "subscription": null | ||||
|         }, | ||||
|         "email": "hamlet@zulip.com", | ||||
|         "id": "cus_D7OT2jf5YAtZQL", | ||||
|         "invoice_prefix": "23ABC45", | ||||
|         "livemode": false, | ||||
|         "metadata": { | ||||
|             "realm_id": "1", | ||||
|             "realm_str": "zulip" | ||||
|         }, | ||||
|         "object": "customer", | ||||
|         "shipping": null, | ||||
|         "sources": { | ||||
|             "data": [ | ||||
|                 { | ||||
|                     "address_city": "Pacific", | ||||
|                     "address_country": "United States", | ||||
|                     "address_line1": "Under the sea", | ||||
|                     "address_line1_check": "pass", | ||||
|                     "address_line2": null, | ||||
|                     "address_state": "FL", | ||||
|                     "address_zip": "33333", | ||||
|                     "address_zip_check": "pass", | ||||
|                     "brand": "Visa", | ||||
|                     "country": "US", | ||||
|                     "customer": "cus_D7OT2jf5YAtZQL", | ||||
|                     "cvc_check": "pass", | ||||
|                     "dynamic_last4": null, | ||||
|                     "exp_month": 3, | ||||
|                     "exp_year": 2033, | ||||
|                     "fingerprint": "6dAXT9VZvwro65EK", | ||||
|                     "funding": "credit", | ||||
|                     "id": "card_1Ch9gVGh0CmXqmnwv94RombT", | ||||
|                     "last4": "4242", | ||||
|                     "metadata": {}, | ||||
|                     "name": "Ada Starr", | ||||
|                     "object": "card", | ||||
|                     "tokenization_method": null | ||||
|                 } | ||||
|             ], | ||||
|             "has_more": false, | ||||
|             "object": "list", | ||||
|             "total_count": 1, | ||||
|             "url": "/v1/customers/cus_D7OT2jf5YAtZQL/sources" | ||||
|         }, | ||||
|         "subscriptions": {} | ||||
|     }, | ||||
|     "create_subscription": { | ||||
|         "application_fee_percent": null, | ||||
|         "billing": "charge_automatically", | ||||
|         "billing_cycle_anchor": 1529990751, | ||||
|         "cancel_at_period_end": false, | ||||
|         "canceled_at": null, | ||||
|         "created": 1529990751, | ||||
|         "current_period_end": 1561526751, | ||||
|         "current_period_start": 1529990751, | ||||
|         "customer": "cus_D7OT2jf5YAtZQL", | ||||
|         "days_until_due": null, | ||||
|         "discount": null, | ||||
|         "ended_at": null, | ||||
|         "id": "sub_D7OTT8FZbOPxah", | ||||
|         "items": { | ||||
|             "data": [ | ||||
|                 { | ||||
|                     "created": 1529990751, | ||||
|                     "id": "si_D7OTEItF5ZLN2R", | ||||
|                     "metadata": {}, | ||||
|                     "object": "subscription_item", | ||||
|                     "plan": { | ||||
|                         "active": true, | ||||
|                         "aggregate_usage": null, | ||||
|                         "amount": 8000, | ||||
|                         "billing_scheme": "per_unit", | ||||
|                         "created": 1529987890, | ||||
|                         "currency": "usd", | ||||
|                         "id": "plan_Do3xCvbzO89OsR", | ||||
|                         "interval": "year", | ||||
|                         "interval_count": 1, | ||||
|                         "livemode": false, | ||||
|                         "metadata": {}, | ||||
|                         "nickname": "annual", | ||||
|                         "object": "plan", | ||||
|                         "product": "prod_D7NhmicJvX2edE", | ||||
|                         "tiers": null, | ||||
|                         "tiers_mode": null, | ||||
|                         "transform_usage": null, | ||||
|                         "trial_period_days": null, | ||||
|                         "usage_type": "licensed" | ||||
|                     }, | ||||
|                     "quantity": 8, | ||||
|                     "subscription": "sub_D7OTT8FZbOPxah" | ||||
|                 } | ||||
|             ], | ||||
|             "has_more": false, | ||||
|             "object": "list", | ||||
|             "total_count": 1, | ||||
|             "url": "/v1/subscription_items?subscription=sub_D7OTT8FZbOPxah" | ||||
|         }, | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "subscription", | ||||
|         "plan": { | ||||
|             "active": true, | ||||
|             "aggregate_usage": null, | ||||
|             "amount": 8000, | ||||
|             "billing_scheme": "per_unit", | ||||
|             "created": 1529987890, | ||||
|             "currency": "usd", | ||||
|             "id": "plan_Do3xCvbzO89OsR", | ||||
|             "interval": "year", | ||||
|             "interval_count": 1, | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "nickname": "annual", | ||||
|             "object": "plan", | ||||
|             "product": "prod_D7NhmicJvX2edE", | ||||
|             "tiers": null, | ||||
|             "tiers_mode": null, | ||||
|             "transform_usage": null, | ||||
|             "trial_period_days": null, | ||||
|             "usage_type": "licensed" | ||||
|         }, | ||||
|         "quantity": 8, | ||||
|         "start": 1529990751, | ||||
|         "status": "active", | ||||
|         "tax_percent": 0.0, | ||||
|         "trial_end": null, | ||||
|         "trial_start": null | ||||
|     }, | ||||
|     "customer_with_subscription": { | ||||
|         "account_balance": 0, | ||||
|         "created": 1529990750, | ||||
|         "currency": "usd", | ||||
|         "default_source": { | ||||
|             "address_city": "Pacific", | ||||
|             "address_country": "United States", | ||||
|             "address_line1": "Under the sea", | ||||
|             "address_line1_check": "pass", | ||||
|             "address_line2": null, | ||||
|             "address_state": "FL", | ||||
|             "address_zip": "33333", | ||||
|             "address_zip_check": "pass", | ||||
|             "brand": "Visa", | ||||
|             "country": "US", | ||||
|             "customer": "cus_D7OT2jf5YAtZQL", | ||||
|             "cvc_check": "pass", | ||||
|             "dynamic_last4": null, | ||||
|             "exp_month": 3, | ||||
|             "exp_year": 2033, | ||||
|             "fingerprint": "6dAXT9VZvwro65EK", | ||||
|             "funding": "credit", | ||||
|             "id": "card_1Ch9gVGh0CmXqmnwv94RombT", | ||||
|             "last4": "4242", | ||||
|             "metadata": {}, | ||||
|             "name": "Ada Starr", | ||||
|             "object": "card", | ||||
|             "tokenization_method": null | ||||
|         }, | ||||
|         "delinquent": false, | ||||
|         "description": "zulip (Zulip Dev)", | ||||
|         "discount": null, | ||||
|         "email": "hamlet@zulip.com", | ||||
|         "id": "cus_D7OT2jf5YAtZQL", | ||||
|         "invoice_prefix": "23ABC45", | ||||
|         "livemode": false, | ||||
|         "metadata": { | ||||
|             "realm_id": "1", | ||||
|             "realm_str": "zulip" | ||||
|         }, | ||||
|         "object": "customer", | ||||
|         "shipping": null, | ||||
|         "sources": { | ||||
|             "data": [ | ||||
|                 { | ||||
|                     "address_city": "Pacific", | ||||
|                     "address_country": "United States", | ||||
|                     "address_line1": "Under the sea", | ||||
|                     "address_line1_check": "pass", | ||||
|                     "address_line2": null, | ||||
|                     "address_state": "FL", | ||||
|                     "address_zip": "33333", | ||||
|                     "address_zip_check": "pass", | ||||
|                     "brand": "Visa", | ||||
|                     "country": "US", | ||||
|                     "customer": "cus_D7OT2jf5YAtZQL", | ||||
|                     "cvc_check": "pass", | ||||
|                     "dynamic_last4": null, | ||||
|                     "exp_month": 3, | ||||
|                     "exp_year": 2033, | ||||
|                     "fingerprint": "6dAXT9VZvwro65EK", | ||||
|                     "funding": "credit", | ||||
|                     "id": "card_1Ch9gVGh0CmXqmnwv94RombT", | ||||
|                     "last4": "4242", | ||||
|                     "metadata": {}, | ||||
|                     "name": "Ada Starr", | ||||
|                     "object": "card", | ||||
|                     "tokenization_method": null | ||||
|                 } | ||||
|             ], | ||||
|             "has_more": false, | ||||
|             "object": "list", | ||||
|             "total_count": 1, | ||||
|             "url": "/v1/customers/cus_D7OT2jf5YAtZQL/sources" | ||||
|         }, | ||||
|         "subscriptions": { | ||||
|             "data": [ | ||||
|                 { | ||||
|                     "application_fee_percent": null, | ||||
|                     "billing": "charge_automatically", | ||||
|                     "billing_cycle_anchor": 1529990751, | ||||
|                     "cancel_at_period_end": false, | ||||
|                     "canceled_at": null, | ||||
|                     "created": 1529990751, | ||||
|                     "current_period_end": 1561526751, | ||||
|                     "current_period_start": 1529990751, | ||||
|                     "customer": "cus_D7OT2jf5YAtZQL", | ||||
|                     "days_until_due": null, | ||||
|                     "discount": null, | ||||
|                     "ended_at": null, | ||||
|                     "id": "sub_D7OTT8FZbOPxah", | ||||
|                     "items": { | ||||
|                         "data": [ | ||||
|                             { | ||||
|                                 "created": 1529990751, | ||||
|                                 "id": "si_D7OTEItF5ZLN2R", | ||||
|                                 "metadata": {}, | ||||
|                                 "object": "subscription_item", | ||||
|                                 "plan": { | ||||
|                                     "active": true, | ||||
|                                     "aggregate_usage": null, | ||||
|                                     "amount": 8000, | ||||
|                                     "billing_scheme": "per_unit", | ||||
|                                     "created": 1529987890, | ||||
|                                     "currency": "usd", | ||||
|                                     "id": "plan_Do3xCvbzO89OsR", | ||||
|                                     "interval": "year", | ||||
|                                     "interval_count": 1, | ||||
|                                     "livemode": false, | ||||
|                                     "metadata": {}, | ||||
|                                     "nickname": "annual", | ||||
|                                     "object": "plan", | ||||
|                                     "product": "prod_D7NhmicJvX2edE", | ||||
|                                     "tiers": null, | ||||
|                                     "tiers_mode": null, | ||||
|                                     "transform_usage": null, | ||||
|                                     "trial_period_days": null, | ||||
|                                     "usage_type": "licensed" | ||||
|                                 }, | ||||
|                                 "quantity": 8, | ||||
|                                 "subscription": "sub_D7OTT8FZbOPxah" | ||||
|                             } | ||||
|                         ], | ||||
|                         "has_more": false, | ||||
|                         "object": "list", | ||||
|                         "total_count": 1, | ||||
|                         "url": "/v1/subscription_items?subscription=sub_D7OTT8FZbOPxah" | ||||
|                     }, | ||||
|                     "livemode": false, | ||||
|                     "metadata": {}, | ||||
|                     "object": "subscription", | ||||
|                     "plan": { | ||||
|                         "active": true, | ||||
|                         "aggregate_usage": null, | ||||
|                         "amount": 8000, | ||||
|                         "billing_scheme": "per_unit", | ||||
|                         "created": 1529987890, | ||||
|                         "currency": "usd", | ||||
|                         "id": "plan_Do3xCvbzO89OsR", | ||||
|                         "interval": "year", | ||||
|                         "interval_count": 1, | ||||
|                         "livemode": false, | ||||
|                         "metadata": {}, | ||||
|                         "nickname": "annual", | ||||
|                         "object": "plan", | ||||
|                         "product": "prod_D7NhmicJvX2edE", | ||||
|                         "tiers": null, | ||||
|                         "tiers_mode": null, | ||||
|                         "transform_usage": null, | ||||
|                         "trial_period_days": null, | ||||
|                         "usage_type": "licensed" | ||||
|                     }, | ||||
|                     "quantity": 8, | ||||
|                     "start": 1529990751, | ||||
|                     "status": "active", | ||||
|                     "tax_percent": 0.0, | ||||
|                     "trial_end": null, | ||||
|                     "trial_start": null | ||||
|                 } | ||||
|             ], | ||||
|             "has_more": false, | ||||
|             "object": "list", | ||||
|             "total_count": 1, | ||||
|             "url": "/v1/customers/cus_D7OT2jf5YAtZQL/subscriptions" | ||||
|         } | ||||
|     }, | ||||
|     "upcoming_invoice": { | ||||
|         "amount_due": 64000, | ||||
|         "amount_paid": 0, | ||||
|         "amount_remaining": 64000, | ||||
|         "application_fee": null, | ||||
|         "attempt_count": 0, | ||||
|         "attempted": false, | ||||
|         "billing": "charge_automatically", | ||||
|         "billing_reason": "upcoming", | ||||
|         "charge": null, | ||||
|         "closed": false, | ||||
|         "currency": "usd", | ||||
|         "customer": "cus_D7OT2jf5YAtZQL", | ||||
|         "date": 1561526751, | ||||
|         "description": "", | ||||
|         "discount": null, | ||||
|         "due_date": null, | ||||
|         "ending_balance": null, | ||||
|         "forgiven": false, | ||||
|         "lines": { | ||||
|             "data": [ | ||||
|                 { | ||||
|                     "amount": 64000, | ||||
|                     "currency": "usd", | ||||
|                     "description": "8 user \u00d7 Zulip Cloud Standard (at $80.00 / year)", | ||||
|                     "discountable": true, | ||||
|                     "id": "sub_D7OTT8FZbOPxah", | ||||
|                     "livemode": false, | ||||
|                     "metadata": {}, | ||||
|                     "object": "line_item", | ||||
|                     "period": { | ||||
|                         "end": 1593149151, | ||||
|                         "start": 1561526751 | ||||
|                     }, | ||||
|                     "plan": { | ||||
|                         "active": true, | ||||
|                         "aggregate_usage": null, | ||||
|                         "amount": 8000, | ||||
|                         "billing_scheme": "per_unit", | ||||
|                         "created": 1529987890, | ||||
|                         "currency": "usd", | ||||
|                         "id": "plan_Do3xCvbzO89OsR", | ||||
|                         "interval": "year", | ||||
|                         "interval_count": 1, | ||||
|                         "livemode": false, | ||||
|                         "metadata": {}, | ||||
|                         "nickname": "annual", | ||||
|                         "object": "plan", | ||||
|                         "product": "prod_D7NhmicJvX2edE", | ||||
|                         "tiers": null, | ||||
|                         "tiers_mode": null, | ||||
|                         "transform_usage": null, | ||||
|                         "trial_period_days": null, | ||||
|                         "usage_type": "licensed" | ||||
|                     }, | ||||
|                     "proration": false, | ||||
|                     "quantity": 8, | ||||
|                     "subscription": null, | ||||
|                     "subscription_item": "si_D7OTEItF5ZLN2R", | ||||
|                     "type": "subscription" | ||||
|                 } | ||||
|             ], | ||||
|             "has_more": false, | ||||
|             "object": "list", | ||||
|             "total_count": 1, | ||||
|             "url": "/v1/invoices/upcoming/lines?customer=cus_D7OT2jf5YAtZQL" | ||||
|         }, | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "next_payment_attempt": 1561530351, | ||||
|         "number": "23ABC45-0002", | ||||
|         "object": "invoice", | ||||
|         "paid": false, | ||||
|         "period_end": 1561526751, | ||||
|         "period_start": 1529990751, | ||||
|         "receipt_number": null, | ||||
|         "starting_balance": 0, | ||||
|         "statement_descriptor": null, | ||||
|         "subscription": "sub_D7OTT8FZbOPxah", | ||||
|         "subtotal": 64000, | ||||
|         "tax": 0, | ||||
|         "tax_percent": 0.0, | ||||
|         "total": 64000, | ||||
|         "webhooks_delivered_at": null | ||||
|     }, | ||||
| } | ||||
| @@ -1,78 +0,0 @@ | ||||
| { | ||||
|   "amount": 7200, | ||||
|   "amount_refunded": 0, | ||||
|   "application": null, | ||||
|   "application_fee": null, | ||||
|   "application_fee_amount": null, | ||||
|   "balance_transaction": "txn_NORMALIZED00000000000001", | ||||
|   "captured": true, | ||||
|   "created": 1000000000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "description": "Upgrade to Zulip Standard, $12.0 x 6", | ||||
|   "destination": null, | ||||
|   "dispute": null, | ||||
|   "failure_code": null, | ||||
|   "failure_message": null, | ||||
|   "fraud_details": {}, | ||||
|   "id": "ch_NORMALIZED00000000000001", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "charge", | ||||
|   "on_behalf_of": null, | ||||
|   "order": null, | ||||
|   "outcome": { | ||||
|     "network_status": "approved_by_network", | ||||
|     "reason": null, | ||||
|     "risk_level": "normal", | ||||
|     "risk_score": 00, | ||||
|     "seller_message": "Payment complete.", | ||||
|     "type": "authorized" | ||||
|   }, | ||||
|   "paid": true, | ||||
|   "payment_intent": null, | ||||
|   "receipt_email": "hamlet@zulip.com", | ||||
|   "receipt_number": null, | ||||
|   "receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000001/rcpt_NORMALIZED000000000000000000001", | ||||
|   "refunded": false, | ||||
|   "refunds": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/charges/ch_NORMALIZED00000000000001/refunds" | ||||
|   }, | ||||
|   "review": null, | ||||
|   "shipping": null, | ||||
|   "source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_NORMALIZED0001", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000001", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000001", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "source_transfer": null, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "succeeded", | ||||
|   "transfer_data": null, | ||||
|   "transfer_group": null | ||||
| } | ||||
| @@ -1,78 +0,0 @@ | ||||
| { | ||||
|   "amount": 36000, | ||||
|   "amount_refunded": 0, | ||||
|   "application": null, | ||||
|   "application_fee": null, | ||||
|   "application_fee_amount": null, | ||||
|   "balance_transaction": "txn_NORMALIZED00000000000002", | ||||
|   "captured": true, | ||||
|   "created": 1000000000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "description": "Upgrade to Zulip Standard, $60.0 x 6", | ||||
|   "destination": null, | ||||
|   "dispute": null, | ||||
|   "failure_code": null, | ||||
|   "failure_message": null, | ||||
|   "fraud_details": {}, | ||||
|   "id": "ch_NORMALIZED00000000000002", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "charge", | ||||
|   "on_behalf_of": null, | ||||
|   "order": null, | ||||
|   "outcome": { | ||||
|     "network_status": "approved_by_network", | ||||
|     "reason": null, | ||||
|     "risk_level": "normal", | ||||
|     "risk_score": 00, | ||||
|     "seller_message": "Payment complete.", | ||||
|     "type": "authorized" | ||||
|   }, | ||||
|   "paid": true, | ||||
|   "payment_intent": null, | ||||
|   "receipt_email": "hamlet@zulip.com", | ||||
|   "receipt_number": null, | ||||
|   "receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000002/rcpt_NORMALIZED000000000000000000002", | ||||
|   "refunded": false, | ||||
|   "refunds": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/charges/ch_NORMALIZED00000000000002/refunds" | ||||
|   }, | ||||
|   "review": null, | ||||
|   "shipping": null, | ||||
|   "source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_NORMALIZED0001", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000001", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000002", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "source_transfer": null, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "succeeded", | ||||
|   "transfer_data": null, | ||||
|   "transfer_group": null | ||||
| } | ||||
| @@ -1,79 +0,0 @@ | ||||
| { | ||||
|   "data": [ | ||||
|     { | ||||
|       "amount": 7200, | ||||
|       "amount_refunded": 0, | ||||
|       "application": null, | ||||
|       "application_fee": null, | ||||
|       "application_fee_amount": null, | ||||
|       "balance_transaction": "txn_NORMALIZED00000000000001", | ||||
|       "captured": true, | ||||
|       "created": 1000000000, | ||||
|       "currency": "usd", | ||||
|       "customer": "cus_NORMALIZED0001", | ||||
|       "description": "Upgrade to Zulip Standard, $12.0 x 6", | ||||
|       "destination": null, | ||||
|       "dispute": null, | ||||
|       "failure_code": null, | ||||
|       "failure_message": null, | ||||
|       "fraud_details": {}, | ||||
|       "id": "ch_NORMALIZED00000000000001", | ||||
|       "invoice": null, | ||||
|       "livemode": false, | ||||
|       "metadata": {}, | ||||
|       "object": "charge", | ||||
|       "on_behalf_of": null, | ||||
|       "order": null, | ||||
|       "outcome": { | ||||
|         "network_status": "approved_by_network", | ||||
|         "reason": null, | ||||
|         "risk_level": "normal", | ||||
|         "risk_score": 00, | ||||
|         "seller_message": "Payment complete.", | ||||
|         "type": "authorized" | ||||
|       }, | ||||
|       "paid": true, | ||||
|       "payment_intent": null, | ||||
|       "receipt_email": "hamlet@zulip.com", | ||||
|       "receipt_number": null, | ||||
|       "receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000001/rcpt_NORMALIZED000000000000000000001", | ||||
|       "refunded": false, | ||||
|       "refunds": {}, | ||||
|       "review": null, | ||||
|       "shipping": null, | ||||
|       "source": { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_NORMALIZED0001", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "NORMALIZED000001", | ||||
|         "funding": "credit", | ||||
|         "id": "card_NORMALIZED00000000000001", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       }, | ||||
|       "source_transfer": null, | ||||
|       "statement_descriptor": "Zulip Standard", | ||||
|       "status": "succeeded", | ||||
|       "transfer_data": null, | ||||
|       "transfer_group": null | ||||
|     } | ||||
|   ], | ||||
|   "has_more": false, | ||||
|   "object": "list", | ||||
|   "url": "/v1/charges" | ||||
| } | ||||
| @@ -1,151 +0,0 @@ | ||||
| { | ||||
|   "data": [ | ||||
|     { | ||||
|       "amount": 36000, | ||||
|       "amount_refunded": 0, | ||||
|       "application": null, | ||||
|       "application_fee": null, | ||||
|       "application_fee_amount": null, | ||||
|       "balance_transaction": "txn_NORMALIZED00000000000002", | ||||
|       "captured": true, | ||||
|       "created": 1000000000, | ||||
|       "currency": "usd", | ||||
|       "customer": "cus_NORMALIZED0001", | ||||
|       "description": "Upgrade to Zulip Standard, $60.0 x 6", | ||||
|       "destination": null, | ||||
|       "dispute": null, | ||||
|       "failure_code": null, | ||||
|       "failure_message": null, | ||||
|       "fraud_details": {}, | ||||
|       "id": "ch_NORMALIZED00000000000002", | ||||
|       "invoice": null, | ||||
|       "livemode": false, | ||||
|       "metadata": {}, | ||||
|       "object": "charge", | ||||
|       "on_behalf_of": null, | ||||
|       "order": null, | ||||
|       "outcome": { | ||||
|         "network_status": "approved_by_network", | ||||
|         "reason": null, | ||||
|         "risk_level": "normal", | ||||
|         "risk_score": 00, | ||||
|         "seller_message": "Payment complete.", | ||||
|         "type": "authorized" | ||||
|       }, | ||||
|       "paid": true, | ||||
|       "payment_intent": null, | ||||
|       "receipt_email": "hamlet@zulip.com", | ||||
|       "receipt_number": null, | ||||
|       "receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000002/rcpt_NORMALIZED000000000000000000002", | ||||
|       "refunded": false, | ||||
|       "refunds": {}, | ||||
|       "review": null, | ||||
|       "shipping": null, | ||||
|       "source": { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_NORMALIZED0001", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "NORMALIZED000001", | ||||
|         "funding": "credit", | ||||
|         "id": "card_NORMALIZED00000000000002", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       }, | ||||
|       "source_transfer": null, | ||||
|       "statement_descriptor": "Zulip Standard", | ||||
|       "status": "succeeded", | ||||
|       "transfer_data": null, | ||||
|       "transfer_group": null | ||||
|     }, | ||||
|     { | ||||
|       "amount": 7200, | ||||
|       "amount_refunded": 0, | ||||
|       "application": null, | ||||
|       "application_fee": null, | ||||
|       "application_fee_amount": null, | ||||
|       "balance_transaction": "txn_NORMALIZED00000000000001", | ||||
|       "captured": true, | ||||
|       "created": 1000000000, | ||||
|       "currency": "usd", | ||||
|       "customer": "cus_NORMALIZED0001", | ||||
|       "description": "Upgrade to Zulip Standard, $12.0 x 6", | ||||
|       "destination": null, | ||||
|       "dispute": null, | ||||
|       "failure_code": null, | ||||
|       "failure_message": null, | ||||
|       "fraud_details": {}, | ||||
|       "id": "ch_NORMALIZED00000000000001", | ||||
|       "invoice": null, | ||||
|       "livemode": false, | ||||
|       "metadata": {}, | ||||
|       "object": "charge", | ||||
|       "on_behalf_of": null, | ||||
|       "order": null, | ||||
|       "outcome": { | ||||
|         "network_status": "approved_by_network", | ||||
|         "reason": null, | ||||
|         "risk_level": "normal", | ||||
|         "risk_score": 00, | ||||
|         "seller_message": "Payment complete.", | ||||
|         "type": "authorized" | ||||
|       }, | ||||
|       "paid": true, | ||||
|       "payment_intent": null, | ||||
|       "receipt_email": "hamlet@zulip.com", | ||||
|       "receipt_number": null, | ||||
|       "receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000001/rcpt_NORMALIZED000000000000000000001", | ||||
|       "refunded": false, | ||||
|       "refunds": {}, | ||||
|       "review": null, | ||||
|       "shipping": null, | ||||
|       "source": { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_NORMALIZED0001", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "NORMALIZED000001", | ||||
|         "funding": "credit", | ||||
|         "id": "card_NORMALIZED00000000000001", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       }, | ||||
|       "source_transfer": null, | ||||
|       "statement_descriptor": "Zulip Standard", | ||||
|       "status": "succeeded", | ||||
|       "transfer_data": null, | ||||
|       "transfer_group": null | ||||
|     } | ||||
|   ], | ||||
|   "has_more": false, | ||||
|   "object": "list", | ||||
|   "url": "/v1/charges" | ||||
| } | ||||
| @@ -1,89 +0,0 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1000000000, | ||||
|   "currency": "usd", | ||||
|   "default_source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_NORMALIZED0001", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000001", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000001", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_NORMALIZED0001", | ||||
|   "invoice_prefix": "NORMA01", | ||||
|   "invoice_settings": { | ||||
|     "custom_fields": null, | ||||
|     "footer": null | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
|     "realm_str": "zulip" | ||||
|   }, | ||||
|   "object": "customer", | ||||
|   "shipping": null, | ||||
|   "sources": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_NORMALIZED0001", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "NORMALIZED000001", | ||||
|         "funding": "credit", | ||||
|         "id": "card_NORMALIZED00000000000001", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| } | ||||
| @@ -1,65 +0,0 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1000000000, | ||||
|   "currency": "usd", | ||||
|   "default_source": "card_NORMALIZED00000000000002", | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_NORMALIZED0001", | ||||
|   "invoice_prefix": "NORMA01", | ||||
|   "invoice_settings": { | ||||
|     "custom_fields": null, | ||||
|     "footer": null | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
|     "realm_str": "zulip" | ||||
|   }, | ||||
|   "object": "customer", | ||||
|   "shipping": null, | ||||
|   "sources": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_NORMALIZED0001", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "NORMALIZED000001", | ||||
|         "funding": "credit", | ||||
|         "id": "card_NORMALIZED00000000000002", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| } | ||||
| @@ -1,92 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 0, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 0, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "auto_advance": true, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": null, | ||||
|   "ending_balance": null, | ||||
|   "finalized_at": null, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": null, | ||||
|   "id": "in_NORMALIZED00000000000001", | ||||
|   "invoice_pdf": null, | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 7200, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000001", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 6, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": -7200, | ||||
|         "currency": "usd", | ||||
|         "description": "Payment (Card ending in 4242)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000002", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 2, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": 1000000000, | ||||
|   "number": "NORMALI-0001", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "draft", | ||||
|   "subscription": null, | ||||
|   "subtotal": 0, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 0, | ||||
|   "webhooks_delivered_at": null | ||||
| } | ||||
| @@ -1,92 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 0, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 0, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "auto_advance": true, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": null, | ||||
|   "ending_balance": null, | ||||
|   "finalized_at": null, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": null, | ||||
|   "id": "in_NORMALIZED00000000000002", | ||||
|   "invoice_pdf": null, | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 36000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000003", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000003", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 6, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": -36000, | ||||
|         "currency": "usd", | ||||
|         "description": "Payment (Card ending in 4242)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000004", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000004", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 2, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": 1000000000, | ||||
|   "number": "NORMALI-0002", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "draft", | ||||
|   "subscription": null, | ||||
|   "subtotal": 0, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 0, | ||||
|   "webhooks_delivered_at": null | ||||
| } | ||||
| @@ -1,92 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 0, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 0, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": true, | ||||
|   "auto_advance": false, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": 1000000000, | ||||
|   "ending_balance": 0, | ||||
|   "finalized_at": 1000000000, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001", | ||||
|   "id": "in_NORMALIZED00000000000001", | ||||
|   "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf", | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 7200, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000001", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 6, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": -7200, | ||||
|         "currency": "usd", | ||||
|         "description": "Payment (Card ending in 4242)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000002", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 2, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": null, | ||||
|   "number": "NORMALI-0001", | ||||
|   "object": "invoice", | ||||
|   "paid": true, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "paid", | ||||
|   "subscription": null, | ||||
|   "subtotal": 0, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 0, | ||||
|   "webhooks_delivered_at": 1000000000 | ||||
| } | ||||
| @@ -1,92 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 0, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 0, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": true, | ||||
|   "auto_advance": false, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": 1000000000, | ||||
|   "ending_balance": 0, | ||||
|   "finalized_at": 1000000000, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002", | ||||
|   "id": "in_NORMALIZED00000000000002", | ||||
|   "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002/pdf", | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 36000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000003", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000003", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 6, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": -36000, | ||||
|         "currency": "usd", | ||||
|         "description": "Payment (Card ending in 4242)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000004", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000004", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 2, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": null, | ||||
|   "number": "NORMALI-0002", | ||||
|   "object": "invoice", | ||||
|   "paid": true, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "paid", | ||||
|   "subscription": null, | ||||
|   "subtotal": 0, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 0, | ||||
|   "webhooks_delivered_at": 1000000000 | ||||
| } | ||||
| @@ -1,99 +0,0 @@ | ||||
| { | ||||
|   "data": [ | ||||
|     { | ||||
|       "amount_due": 0, | ||||
|       "amount_paid": 0, | ||||
|       "amount_remaining": 0, | ||||
|       "application_fee": null, | ||||
|       "attempt_count": 0, | ||||
|       "attempted": true, | ||||
|       "auto_advance": false, | ||||
|       "billing": "charge_automatically", | ||||
|       "billing_reason": "manual", | ||||
|       "charge": null, | ||||
|       "currency": "usd", | ||||
|       "custom_fields": null, | ||||
|       "customer": "cus_NORMALIZED0001", | ||||
|       "date": 1000000000, | ||||
|       "default_source": null, | ||||
|       "description": "", | ||||
|       "discount": null, | ||||
|       "due_date": 1000000000, | ||||
|       "ending_balance": 0, | ||||
|       "finalized_at": 1000000000, | ||||
|       "footer": null, | ||||
|       "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001", | ||||
|       "id": "in_NORMALIZED00000000000001", | ||||
|       "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf", | ||||
|       "lines": { | ||||
|         "data": [ | ||||
|           { | ||||
|             "amount": 7200, | ||||
|             "currency": "usd", | ||||
|             "description": "Zulip Standard", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000001", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1000000000, | ||||
|               "start": 1000000000 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 6, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           }, | ||||
|           { | ||||
|             "amount": -7200, | ||||
|             "currency": "usd", | ||||
|             "description": "Payment (Card ending in 4242)", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000002", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1000000000, | ||||
|               "start": 1000000000 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 1, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           } | ||||
|         ], | ||||
|         "has_more": false, | ||||
|         "object": "list", | ||||
|         "total_count": 2, | ||||
|         "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|       }, | ||||
|       "livemode": false, | ||||
|       "metadata": {}, | ||||
|       "next_payment_attempt": null, | ||||
|       "number": "NORMALI-0001", | ||||
|       "object": "invoice", | ||||
|       "paid": true, | ||||
|       "period_end": 1000000000, | ||||
|       "period_start": 1000000000, | ||||
|       "receipt_number": null, | ||||
|       "starting_balance": 0, | ||||
|       "statement_descriptor": "Zulip Standard", | ||||
|       "status": "paid", | ||||
|       "subscription": null, | ||||
|       "subtotal": 0, | ||||
|       "tax": 0, | ||||
|       "tax_percent": null, | ||||
|       "total": 0, | ||||
|       "webhooks_delivered_at": 1000000000 | ||||
|     } | ||||
|   ], | ||||
|   "has_more": false, | ||||
|   "object": "list", | ||||
|   "url": "/v1/invoices" | ||||
| } | ||||
| @@ -1,191 +0,0 @@ | ||||
| { | ||||
|   "data": [ | ||||
|     { | ||||
|       "amount_due": 0, | ||||
|       "amount_paid": 0, | ||||
|       "amount_remaining": 0, | ||||
|       "application_fee": null, | ||||
|       "attempt_count": 0, | ||||
|       "attempted": true, | ||||
|       "auto_advance": false, | ||||
|       "billing": "charge_automatically", | ||||
|       "billing_reason": "manual", | ||||
|       "charge": null, | ||||
|       "currency": "usd", | ||||
|       "custom_fields": null, | ||||
|       "customer": "cus_NORMALIZED0001", | ||||
|       "date": 1000000000, | ||||
|       "default_source": null, | ||||
|       "description": "", | ||||
|       "discount": null, | ||||
|       "due_date": 1000000000, | ||||
|       "ending_balance": 0, | ||||
|       "finalized_at": 1000000000, | ||||
|       "footer": null, | ||||
|       "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002", | ||||
|       "id": "in_NORMALIZED00000000000002", | ||||
|       "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002/pdf", | ||||
|       "lines": { | ||||
|         "data": [ | ||||
|           { | ||||
|             "amount": 36000, | ||||
|             "currency": "usd", | ||||
|             "description": "Zulip Standard", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000003", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000003", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1000000000, | ||||
|               "start": 1000000000 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 6, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           }, | ||||
|           { | ||||
|             "amount": -36000, | ||||
|             "currency": "usd", | ||||
|             "description": "Payment (Card ending in 4242)", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000004", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000004", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1000000000, | ||||
|               "start": 1000000000 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 1, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           } | ||||
|         ], | ||||
|         "has_more": false, | ||||
|         "object": "list", | ||||
|         "total_count": 2, | ||||
|         "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" | ||||
|       }, | ||||
|       "livemode": false, | ||||
|       "metadata": {}, | ||||
|       "next_payment_attempt": null, | ||||
|       "number": "NORMALI-0002", | ||||
|       "object": "invoice", | ||||
|       "paid": true, | ||||
|       "period_end": 1000000000, | ||||
|       "period_start": 1000000000, | ||||
|       "receipt_number": null, | ||||
|       "starting_balance": 0, | ||||
|       "statement_descriptor": "Zulip Standard", | ||||
|       "status": "paid", | ||||
|       "subscription": null, | ||||
|       "subtotal": 0, | ||||
|       "tax": 0, | ||||
|       "tax_percent": null, | ||||
|       "total": 0, | ||||
|       "webhooks_delivered_at": 1000000000 | ||||
|     }, | ||||
|     { | ||||
|       "amount_due": 0, | ||||
|       "amount_paid": 0, | ||||
|       "amount_remaining": 0, | ||||
|       "application_fee": null, | ||||
|       "attempt_count": 0, | ||||
|       "attempted": true, | ||||
|       "auto_advance": false, | ||||
|       "billing": "charge_automatically", | ||||
|       "billing_reason": "manual", | ||||
|       "charge": null, | ||||
|       "currency": "usd", | ||||
|       "custom_fields": null, | ||||
|       "customer": "cus_NORMALIZED0001", | ||||
|       "date": 1000000000, | ||||
|       "default_source": null, | ||||
|       "description": "", | ||||
|       "discount": null, | ||||
|       "due_date": 1000000000, | ||||
|       "ending_balance": 0, | ||||
|       "finalized_at": 1000000000, | ||||
|       "footer": null, | ||||
|       "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001", | ||||
|       "id": "in_NORMALIZED00000000000001", | ||||
|       "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf", | ||||
|       "lines": { | ||||
|         "data": [ | ||||
|           { | ||||
|             "amount": 7200, | ||||
|             "currency": "usd", | ||||
|             "description": "Zulip Standard", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000001", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1000000000, | ||||
|               "start": 1000000000 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 6, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           }, | ||||
|           { | ||||
|             "amount": -7200, | ||||
|             "currency": "usd", | ||||
|             "description": "Payment (Card ending in 4242)", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000002", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1000000000, | ||||
|               "start": 1000000000 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 1, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           } | ||||
|         ], | ||||
|         "has_more": false, | ||||
|         "object": "list", | ||||
|         "total_count": 2, | ||||
|         "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|       }, | ||||
|       "livemode": false, | ||||
|       "metadata": {}, | ||||
|       "next_payment_attempt": null, | ||||
|       "number": "NORMALI-0001", | ||||
|       "object": "invoice", | ||||
|       "paid": true, | ||||
|       "period_end": 1000000000, | ||||
|       "period_start": 1000000000, | ||||
|       "receipt_number": null, | ||||
|       "starting_balance": 0, | ||||
|       "statement_descriptor": "Zulip Standard", | ||||
|       "status": "paid", | ||||
|       "subscription": null, | ||||
|       "subtotal": 0, | ||||
|       "tax": 0, | ||||
|       "tax_percent": null, | ||||
|       "total": 0, | ||||
|       "webhooks_delivered_at": 1000000000 | ||||
|     } | ||||
|   ], | ||||
|   "has_more": false, | ||||
|   "object": "list", | ||||
|   "url": "/v1/invoices" | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": -7200, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Payment (Card ending in 4242)", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000002", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1000000000, | ||||
|     "start": 1000000000 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 1, | ||||
|   "subscription": null, | ||||
|   "unit_amount": -7200 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": 7200, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Zulip Standard", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000001", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1000000000, | ||||
|     "start": 1000000000 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 6, | ||||
|   "subscription": null, | ||||
|   "unit_amount": 1200 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": -36000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Payment (Card ending in 4242)", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000004", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1000000000, | ||||
|     "start": 1000000000 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 1, | ||||
|   "subscription": null, | ||||
|   "unit_amount": -36000 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": 36000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Zulip Standard", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000003", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1000000000, | ||||
|     "start": 1000000000 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 6, | ||||
|   "subscription": null, | ||||
|   "unit_amount": 6000 | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| { | ||||
|   "card": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "unchecked", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "unchecked", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "cvc_check": "unchecked", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000001", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000001", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "client_ip": "0.0.0.0", | ||||
|   "created": 1000000000, | ||||
|   "id": "tok_NORMALIZED00000000000001", | ||||
|   "livemode": false, | ||||
|   "object": "token", | ||||
|   "type": "card", | ||||
|   "used": false | ||||
| } | ||||
| @@ -1,78 +0,0 @@ | ||||
| { | ||||
|   "amount": 48000, | ||||
|   "amount_refunded": 0, | ||||
|   "application": null, | ||||
|   "application_fee": null, | ||||
|   "application_fee_amount": null, | ||||
|   "balance_transaction": "txn_NORMALIZED00000000000001", | ||||
|   "captured": true, | ||||
|   "created": 1000000000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "description": "Upgrade to Zulip Standard, $80.0 x 6", | ||||
|   "destination": null, | ||||
|   "dispute": null, | ||||
|   "failure_code": null, | ||||
|   "failure_message": null, | ||||
|   "fraud_details": {}, | ||||
|   "id": "ch_NORMALIZED00000000000001", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "charge", | ||||
|   "on_behalf_of": null, | ||||
|   "order": null, | ||||
|   "outcome": { | ||||
|     "network_status": "approved_by_network", | ||||
|     "reason": null, | ||||
|     "risk_level": "normal", | ||||
|     "risk_score": 00, | ||||
|     "seller_message": "Payment complete.", | ||||
|     "type": "authorized" | ||||
|   }, | ||||
|   "paid": true, | ||||
|   "payment_intent": null, | ||||
|   "receipt_email": "hamlet@zulip.com", | ||||
|   "receipt_number": null, | ||||
|   "receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000001/rcpt_NORMALIZED000000000000000000001", | ||||
|   "refunded": false, | ||||
|   "refunds": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/charges/ch_NORMALIZED00000000000001/refunds" | ||||
|   }, | ||||
|   "review": null, | ||||
|   "shipping": null, | ||||
|   "source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_NORMALIZED0001", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000001", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000001", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "source_transfer": null, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "succeeded", | ||||
|   "transfer_data": null, | ||||
|   "transfer_group": null | ||||
| } | ||||
| @@ -1,89 +0,0 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1000000000, | ||||
|   "currency": "usd", | ||||
|   "default_source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_NORMALIZED0001", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000001", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000001", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_NORMALIZED0001", | ||||
|   "invoice_prefix": "NORMA01", | ||||
|   "invoice_settings": { | ||||
|     "custom_fields": null, | ||||
|     "footer": null | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
|     "realm_str": "zulip" | ||||
|   }, | ||||
|   "object": "customer", | ||||
|   "shipping": null, | ||||
|   "sources": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_NORMALIZED0001", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "NORMALIZED000001", | ||||
|         "funding": "credit", | ||||
|         "id": "card_NORMALIZED00000000000001", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| } | ||||
| @@ -1,89 +0,0 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1000000000, | ||||
|   "currency": "usd", | ||||
|   "default_source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_NORMALIZED0001", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000001", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000001", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_NORMALIZED0001", | ||||
|   "invoice_prefix": "NORMA01", | ||||
|   "invoice_settings": { | ||||
|     "custom_fields": null, | ||||
|     "footer": null | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
|     "realm_str": "zulip" | ||||
|   }, | ||||
|   "object": "customer", | ||||
|   "shipping": null, | ||||
|   "sources": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_NORMALIZED0001", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "NORMALIZED000001", | ||||
|         "funding": "credit", | ||||
|         "id": "card_NORMALIZED00000000000001", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| } | ||||
| @@ -1,92 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 0, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 0, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "auto_advance": true, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": null, | ||||
|   "ending_balance": null, | ||||
|   "finalized_at": null, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": null, | ||||
|   "id": "in_NORMALIZED00000000000001", | ||||
|   "invoice_pdf": null, | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 48000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000001", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 6, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": -48000, | ||||
|         "currency": "usd", | ||||
|         "description": "Payment (Card ending in 4242)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000002", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 2, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": 1000000000, | ||||
|   "number": "NORMALI-0001", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "draft", | ||||
|   "subscription": null, | ||||
|   "subtotal": 0, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 0, | ||||
|   "webhooks_delivered_at": null | ||||
| } | ||||
| @@ -1,92 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 0, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 0, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": true, | ||||
|   "auto_advance": false, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": 1000000000, | ||||
|   "ending_balance": 0, | ||||
|   "finalized_at": 1000000000, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001", | ||||
|   "id": "in_NORMALIZED00000000000001", | ||||
|   "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf", | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 48000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000001", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 6, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": -48000, | ||||
|         "currency": "usd", | ||||
|         "description": "Payment (Card ending in 4242)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000002", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 2, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": null, | ||||
|   "number": "NORMALI-0001", | ||||
|   "object": "invoice", | ||||
|   "paid": true, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "paid", | ||||
|   "subscription": null, | ||||
|   "subtotal": 0, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 0, | ||||
|   "webhooks_delivered_at": 1000000000 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": -48000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Payment (Card ending in 4242)", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000002", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1000000000, | ||||
|     "start": 1000000000 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 1, | ||||
|   "subscription": null, | ||||
|   "unit_amount": -48000 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": 48000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Zulip Standard", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000001", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1000000000, | ||||
|     "start": 1000000000 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 6, | ||||
|   "subscription": null, | ||||
|   "unit_amount": 8000 | ||||
| } | ||||
| @@ -1,18 +1,14 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1548695523, | ||||
|   "created": 1539881153, | ||||
|   "currency": null, | ||||
|   "default_source": "card_1DxdeRGh0CmXqmnwu3BibWtM", | ||||
|   "default_source": "card_1DMedAGh0CmXqmnwDLwrAV1v", | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_EQUd48LphR5ahk", | ||||
|   "invoice_prefix": "9F53235", | ||||
|   "invoice_settings": { | ||||
|     "custom_fields": null, | ||||
|     "footer": null | ||||
|   }, | ||||
|   "id": "cus_DoHBcS2dBGOP9t", | ||||
|   "invoice_prefix": "3B3F5D6", | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
| @@ -33,14 +29,14 @@ | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_EQUd48LphR5ahk", | ||||
|         "customer": "cus_DoHBcS2dBGOP9t", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "6dAXT9VZvwro65EK", | ||||
|         "funding": "credit", | ||||
|         "id": "card_1DxdeRGh0CmXqmnwu3BibWtM", | ||||
|         "id": "card_1DMedAGh0CmXqmnwDLwrAV1v", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
| @@ -51,14 +47,14 @@ | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_EQUd48LphR5ahk/sources" | ||||
|     "url": "/v1/customers/cus_DoHBcS2dBGOP9t/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/customers/cus_EQUd48LphR5ahk/subscriptions" | ||||
|     "url": "/v1/customers/cus_DoHBcS2dBGOP9t/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| @@ -0,0 +1,168 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1539881153, | ||||
|   "currency": "usd", | ||||
|   "default_source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_DoHBcS2dBGOP9t", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "6dAXT9VZvwro65EK", | ||||
|     "funding": "credit", | ||||
|     "id": "card_1DMedAGh0CmXqmnwDLwrAV1v", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_DoHBcS2dBGOP9t", | ||||
|   "invoice_prefix": "3B3F5D6", | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
|     "realm_str": "zulip" | ||||
|   }, | ||||
|   "object": "customer", | ||||
|   "shipping": null, | ||||
|   "sources": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_DoHBcS2dBGOP9t", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "6dAXT9VZvwro65EK", | ||||
|         "funding": "credit", | ||||
|         "id": "card_1DMedAGh0CmXqmnwDLwrAV1v", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_DoHBcS2dBGOP9t/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "application_fee_percent": null, | ||||
|         "billing": "charge_automatically", | ||||
|         "billing_cycle_anchor": 1539881154, | ||||
|         "cancel_at_period_end": false, | ||||
|         "canceled_at": null, | ||||
|         "created": 1539881154, | ||||
|         "current_period_end": 1571417154, | ||||
|         "current_period_start": 1539881154, | ||||
|         "customer": "cus_DoHBcS2dBGOP9t", | ||||
|         "days_until_due": null, | ||||
|         "discount": null, | ||||
|         "ended_at": null, | ||||
|         "id": "sub_DoHBD49xn11qGo", | ||||
|         "items": { | ||||
|           "data": [ | ||||
|             { | ||||
|               "created": 1539881154, | ||||
|               "id": "si_DoHB9flY9e7zrZ", | ||||
|               "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_DoHBD49xn11qGo" | ||||
|             } | ||||
|           ], | ||||
|           "has_more": false, | ||||
|           "object": "list", | ||||
|           "total_count": 1, | ||||
|           "url": "/v1/subscription_items?subscription=sub_DoHBD49xn11qGo" | ||||
|         }, | ||||
|         "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": 1539881154, | ||||
|         "status": "active", | ||||
|         "tax_percent": 0.0, | ||||
|         "trial_end": null, | ||||
|         "trial_start": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_DoHBcS2dBGOP9t/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| } | ||||
| @@ -0,0 +1,168 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1539881153, | ||||
|   "currency": "usd", | ||||
|   "default_source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_DoHBcS2dBGOP9t", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "6dAXT9VZvwro65EK", | ||||
|     "funding": "credit", | ||||
|     "id": "card_1DMedAGh0CmXqmnwDLwrAV1v", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_DoHBcS2dBGOP9t", | ||||
|   "invoice_prefix": "3B3F5D6", | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
|     "realm_str": "zulip" | ||||
|   }, | ||||
|   "object": "customer", | ||||
|   "shipping": null, | ||||
|   "sources": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_DoHBcS2dBGOP9t", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "6dAXT9VZvwro65EK", | ||||
|         "funding": "credit", | ||||
|         "id": "card_1DMedAGh0CmXqmnwDLwrAV1v", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_DoHBcS2dBGOP9t/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "application_fee_percent": null, | ||||
|         "billing": "charge_automatically", | ||||
|         "billing_cycle_anchor": 1539881154, | ||||
|         "cancel_at_period_end": false, | ||||
|         "canceled_at": null, | ||||
|         "created": 1539881154, | ||||
|         "current_period_end": 1571417154, | ||||
|         "current_period_start": 1539881154, | ||||
|         "customer": "cus_DoHBcS2dBGOP9t", | ||||
|         "days_until_due": null, | ||||
|         "discount": null, | ||||
|         "ended_at": null, | ||||
|         "id": "sub_DoHBD49xn11qGo", | ||||
|         "items": { | ||||
|           "data": [ | ||||
|             { | ||||
|               "created": 1539881154, | ||||
|               "id": "si_DoHB9flY9e7zrZ", | ||||
|               "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_DoHBD49xn11qGo" | ||||
|             } | ||||
|           ], | ||||
|           "has_more": false, | ||||
|           "object": "list", | ||||
|           "total_count": 1, | ||||
|           "url": "/v1/subscription_items?subscription=sub_DoHBD49xn11qGo" | ||||
|         }, | ||||
|         "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": 1539881154, | ||||
|         "status": "active", | ||||
|         "tax_percent": 0.0, | ||||
|         "trial_end": null, | ||||
|         "trial_start": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_DoHBcS2dBGOP9t/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| } | ||||
| @@ -0,0 +1,85 @@ | ||||
| { | ||||
|   "amount_due": 64000, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 64000, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "upcoming", | ||||
|   "charge": null, | ||||
|   "closed": false, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_DoHBcS2dBGOP9t", | ||||
|   "date": 1571417154, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": null, | ||||
|   "ending_balance": 0, | ||||
|   "forgiven": false, | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 64000, | ||||
|         "currency": "usd", | ||||
|         "description": "8 user \u00d7 Zulip Cloud Standard (at $80.00 / year)", | ||||
|         "discountable": true, | ||||
|         "id": "sli_31180ead97e161", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1603039554, | ||||
|           "start": 1571417154 | ||||
|         }, | ||||
|         "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_DoHBD49xn11qGo", | ||||
|         "subscription_item": "si_DoHB9flY9e7zrZ", | ||||
|         "type": "subscription" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/invoices/upcoming/lines?customer=cus_DoHBcS2dBGOP9t" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": 1571420754, | ||||
|   "number": "3B3F5D6-0002", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1571417154, | ||||
|   "period_start": 1539881154, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": null, | ||||
|   "subscription": "sub_DoHBD49xn11qGo", | ||||
|   "subtotal": 64000, | ||||
|   "tax": 0, | ||||
|   "tax_percent": 0.0, | ||||
|   "total": 64000, | ||||
|   "webhooks_delivered_at": null | ||||
| } | ||||
| @@ -0,0 +1,85 @@ | ||||
| { | ||||
|   "amount_due": 64000, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 64000, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "upcoming", | ||||
|   "charge": null, | ||||
|   "closed": false, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_DoHBcS2dBGOP9t", | ||||
|   "date": 1571417154, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": null, | ||||
|   "ending_balance": 0, | ||||
|   "forgiven": false, | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 64000, | ||||
|         "currency": "usd", | ||||
|         "description": "8 user \u00d7 Zulip Cloud Standard (at $80.00 / year)", | ||||
|         "discountable": true, | ||||
|         "id": "sli_31180ead97e161", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1603039554, | ||||
|           "start": 1571417154 | ||||
|         }, | ||||
|         "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_DoHBD49xn11qGo", | ||||
|         "subscription_item": "si_DoHB9flY9e7zrZ", | ||||
|         "type": "subscription" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/invoices/upcoming/lines?customer=cus_DoHBcS2dBGOP9t" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": 1571420754, | ||||
|   "number": "3B3F5D6-0002", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1571417154, | ||||
|   "period_start": 1539881154, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": null, | ||||
|   "subscription": "sub_DoHBD49xn11qGo", | ||||
|   "subtotal": 64000, | ||||
|   "tax": 0, | ||||
|   "tax_percent": 0.0, | ||||
|   "total": 64000, | ||||
|   "webhooks_delivered_at": null | ||||
| } | ||||
| @@ -0,0 +1,82 @@ | ||||
| { | ||||
|   "application_fee_percent": null, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_cycle_anchor": 1539881154, | ||||
|   "cancel_at_period_end": false, | ||||
|   "canceled_at": null, | ||||
|   "created": 1539881154, | ||||
|   "current_period_end": 1571417154, | ||||
|   "current_period_start": 1539881154, | ||||
|   "customer": "cus_DoHBcS2dBGOP9t", | ||||
|   "days_until_due": null, | ||||
|   "discount": null, | ||||
|   "ended_at": null, | ||||
|   "id": "sub_DoHBD49xn11qGo", | ||||
|   "items": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "created": 1539881154, | ||||
|         "id": "si_DoHB9flY9e7zrZ", | ||||
|         "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_DoHBD49xn11qGo" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/subscription_items?subscription=sub_DoHBD49xn11qGo" | ||||
|   }, | ||||
|   "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": 1539881154, | ||||
|   "status": "active", | ||||
|   "tax_percent": 0.0, | ||||
|   "trial_end": null, | ||||
|   "trial_start": null | ||||
| } | ||||
| @@ -16,7 +16,7 @@ | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "6dAXT9VZvwro65EK", | ||||
|     "funding": "credit", | ||||
|     "id": "card_1DxdeRGh0CmXqmnwu3BibWtM", | ||||
|     "id": "card_1DMedAGh0CmXqmnwDLwrAV1v", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
| @@ -24,8 +24,8 @@ | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "client_ip": "107.202.144.213", | ||||
|   "created": 1548695523, | ||||
|   "id": "tok_1DxdeRGh0CmXqmnw5X6CeYnC", | ||||
|   "created": 1539881152, | ||||
|   "id": "tok_1DMedAGh0CmXqmnwduQE6C3S", | ||||
|   "livemode": false, | ||||
|   "object": "token", | ||||
|   "type": "card", | ||||
| @@ -1,39 +0,0 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1000000000, | ||||
|   "currency": null, | ||||
|   "default_source": null, | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_NORMALIZED0001", | ||||
|   "invoice_prefix": "NORMA01", | ||||
|   "invoice_settings": { | ||||
|     "custom_fields": null, | ||||
|     "footer": null | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
|     "realm_str": "zulip" | ||||
|   }, | ||||
|   "object": "customer", | ||||
|   "shipping": null, | ||||
|   "sources": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| } | ||||
| @@ -1,72 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 984000, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 984000, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "auto_advance": true, | ||||
|   "billing": "send_invoice", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": 1000000000, | ||||
|   "ending_balance": null, | ||||
|   "finalized_at": null, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": null, | ||||
|   "id": "in_NORMALIZED00000000000001", | ||||
|   "invoice_pdf": null, | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 984000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000001", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1357095845, | ||||
|           "start": 1325473445 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 123, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": null, | ||||
|   "number": "NORMALI-0001", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "draft", | ||||
|   "subscription": null, | ||||
|   "subtotal": 984000, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 984000, | ||||
|   "webhooks_delivered_at": null | ||||
| } | ||||
| @@ -1,72 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 100, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 100, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "auto_advance": true, | ||||
|   "billing": "send_invoice", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": 1000000000, | ||||
|   "ending_balance": null, | ||||
|   "finalized_at": null, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": null, | ||||
|   "id": "in_NORMALIZED00000000000002", | ||||
|   "invoice_pdf": null, | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 100, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard - renewal", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000002", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1388631845, | ||||
|           "start": 1357095845 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": null, | ||||
|   "number": "NORMALI-0002", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "draft", | ||||
|   "subscription": null, | ||||
|   "subtotal": 100, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 100, | ||||
|   "webhooks_delivered_at": null | ||||
| } | ||||
| @@ -1,72 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 984000, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 984000, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "auto_advance": true, | ||||
|   "billing": "send_invoice", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": 1000000000, | ||||
|   "ending_balance": 0, | ||||
|   "finalized_at": 1000000000, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001", | ||||
|   "id": "in_NORMALIZED00000000000001", | ||||
|   "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf", | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 984000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000001", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1357095845, | ||||
|           "start": 1325473445 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 123, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": null, | ||||
|   "number": "NORMALI-0001", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "open", | ||||
|   "subscription": null, | ||||
|   "subtotal": 984000, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 984000, | ||||
|   "webhooks_delivered_at": 1000000000 | ||||
| } | ||||
| @@ -1,72 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 100, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 100, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "auto_advance": true, | ||||
|   "billing": "send_invoice", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": 1000000000, | ||||
|   "ending_balance": 0, | ||||
|   "finalized_at": 1000000000, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002", | ||||
|   "id": "in_NORMALIZED00000000000002", | ||||
|   "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002/pdf", | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 100, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard - renewal", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000002", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1388631845, | ||||
|           "start": 1357095845 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": null, | ||||
|   "number": "NORMALI-0002", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "open", | ||||
|   "subscription": null, | ||||
|   "subtotal": 100, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 100, | ||||
|   "webhooks_delivered_at": 1000000000 | ||||
| } | ||||
| @@ -1,151 +0,0 @@ | ||||
| { | ||||
|   "data": [ | ||||
|     { | ||||
|       "amount_due": 100, | ||||
|       "amount_paid": 0, | ||||
|       "amount_remaining": 100, | ||||
|       "application_fee": null, | ||||
|       "attempt_count": 0, | ||||
|       "attempted": false, | ||||
|       "auto_advance": true, | ||||
|       "billing": "send_invoice", | ||||
|       "billing_reason": "manual", | ||||
|       "charge": null, | ||||
|       "currency": "usd", | ||||
|       "custom_fields": null, | ||||
|       "customer": "cus_NORMALIZED0001", | ||||
|       "date": 1000000000, | ||||
|       "default_source": null, | ||||
|       "description": "", | ||||
|       "discount": null, | ||||
|       "due_date": 1000000000, | ||||
|       "ending_balance": 0, | ||||
|       "finalized_at": 1000000000, | ||||
|       "footer": null, | ||||
|       "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002", | ||||
|       "id": "in_NORMALIZED00000000000002", | ||||
|       "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002/pdf", | ||||
|       "lines": { | ||||
|         "data": [ | ||||
|           { | ||||
|             "amount": 100, | ||||
|             "currency": "usd", | ||||
|             "description": "Zulip Standard - renewal", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000002", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1388631845, | ||||
|               "start": 1357095845 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 1, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           } | ||||
|         ], | ||||
|         "has_more": false, | ||||
|         "object": "list", | ||||
|         "total_count": 1, | ||||
|         "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" | ||||
|       }, | ||||
|       "livemode": false, | ||||
|       "metadata": {}, | ||||
|       "next_payment_attempt": null, | ||||
|       "number": "NORMALI-0002", | ||||
|       "object": "invoice", | ||||
|       "paid": false, | ||||
|       "period_end": 1000000000, | ||||
|       "period_start": 1000000000, | ||||
|       "receipt_number": null, | ||||
|       "starting_balance": 0, | ||||
|       "statement_descriptor": "Zulip Standard", | ||||
|       "status": "open", | ||||
|       "subscription": null, | ||||
|       "subtotal": 100, | ||||
|       "tax": 0, | ||||
|       "tax_percent": null, | ||||
|       "total": 100, | ||||
|       "webhooks_delivered_at": 1000000000 | ||||
|     }, | ||||
|     { | ||||
|       "amount_due": 984000, | ||||
|       "amount_paid": 0, | ||||
|       "amount_remaining": 984000, | ||||
|       "application_fee": null, | ||||
|       "attempt_count": 0, | ||||
|       "attempted": false, | ||||
|       "auto_advance": true, | ||||
|       "billing": "send_invoice", | ||||
|       "billing_reason": "manual", | ||||
|       "charge": null, | ||||
|       "currency": "usd", | ||||
|       "custom_fields": null, | ||||
|       "customer": "cus_NORMALIZED0001", | ||||
|       "date": 1000000000, | ||||
|       "default_source": null, | ||||
|       "description": "", | ||||
|       "discount": null, | ||||
|       "due_date": 1000000000, | ||||
|       "ending_balance": 0, | ||||
|       "finalized_at": 1000000000, | ||||
|       "footer": null, | ||||
|       "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001", | ||||
|       "id": "in_NORMALIZED00000000000001", | ||||
|       "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf", | ||||
|       "lines": { | ||||
|         "data": [ | ||||
|           { | ||||
|             "amount": 984000, | ||||
|             "currency": "usd", | ||||
|             "description": "Zulip Standard", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000001", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1357095845, | ||||
|               "start": 1325473445 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 123, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           } | ||||
|         ], | ||||
|         "has_more": false, | ||||
|         "object": "list", | ||||
|         "total_count": 1, | ||||
|         "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|       }, | ||||
|       "livemode": false, | ||||
|       "metadata": {}, | ||||
|       "next_payment_attempt": null, | ||||
|       "number": "NORMALI-0001", | ||||
|       "object": "invoice", | ||||
|       "paid": false, | ||||
|       "period_end": 1000000000, | ||||
|       "period_start": 1000000000, | ||||
|       "receipt_number": null, | ||||
|       "starting_balance": 0, | ||||
|       "statement_descriptor": "Zulip Standard", | ||||
|       "status": "open", | ||||
|       "subscription": null, | ||||
|       "subtotal": 984000, | ||||
|       "tax": 0, | ||||
|       "tax_percent": null, | ||||
|       "total": 984000, | ||||
|       "webhooks_delivered_at": 1000000000 | ||||
|     } | ||||
|   ], | ||||
|   "has_more": false, | ||||
|   "object": "list", | ||||
|   "url": "/v1/invoices" | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": 984000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Zulip Standard", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000001", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1357095845, | ||||
|     "start": 1325473445 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 123, | ||||
|   "subscription": null, | ||||
|   "unit_amount": 8000 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": 100, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Zulip Standard - renewal", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000002", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1388631845, | ||||
|     "start": 1357095845 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 1, | ||||
|   "subscription": null, | ||||
|   "unit_amount": 100 | ||||
| } | ||||
| @@ -1,18 +1,14 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1000000000, | ||||
|   "created": 1539832157, | ||||
|   "currency": null, | ||||
|   "default_source": "card_NORMALIZED00000000000001", | ||||
|   "default_source": "card_1DMRsvGh0CmXqmnwRFN7yRRD", | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_NORMALIZED0001", | ||||
|   "invoice_prefix": "NORMA01", | ||||
|   "invoice_settings": { | ||||
|     "custom_fields": null, | ||||
|     "footer": null | ||||
|   }, | ||||
|   "id": "cus_Do40UO0WhJ4ZIl", | ||||
|   "invoice_prefix": "7783290", | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
| @@ -33,14 +29,14 @@ | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_NORMALIZED0001", | ||||
|         "customer": "cus_Do40UO0WhJ4ZIl", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "NORMALIZED000001", | ||||
|         "fingerprint": "6dAXT9VZvwro65EK", | ||||
|         "funding": "credit", | ||||
|         "id": "card_NORMALIZED00000000000001", | ||||
|         "id": "card_1DMRsvGh0CmXqmnwRFN7yRRD", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
| @@ -51,14 +47,14 @@ | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/sources" | ||||
|     "url": "/v1/customers/cus_Do40UO0WhJ4ZIl/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/subscriptions" | ||||
|     "url": "/v1/customers/cus_Do40UO0WhJ4ZIl/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| @@ -0,0 +1,168 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1539832157, | ||||
|   "currency": "usd", | ||||
|   "default_source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_Do40UO0WhJ4ZIl", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "6dAXT9VZvwro65EK", | ||||
|     "funding": "credit", | ||||
|     "id": "card_1DMRsvGh0CmXqmnwRFN7yRRD", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_Do40UO0WhJ4ZIl", | ||||
|   "invoice_prefix": "7783290", | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
|     "realm_str": "zulip" | ||||
|   }, | ||||
|   "object": "customer", | ||||
|   "shipping": null, | ||||
|   "sources": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_Do40UO0WhJ4ZIl", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "6dAXT9VZvwro65EK", | ||||
|         "funding": "credit", | ||||
|         "id": "card_1DMRsvGh0CmXqmnwRFN7yRRD", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_Do40UO0WhJ4ZIl/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "application_fee_percent": null, | ||||
|         "billing": "charge_automatically", | ||||
|         "billing_cycle_anchor": 1539832158, | ||||
|         "cancel_at_period_end": false, | ||||
|         "canceled_at": null, | ||||
|         "created": 1539832158, | ||||
|         "current_period_end": 1571368158, | ||||
|         "current_period_start": 1539832158, | ||||
|         "customer": "cus_Do40UO0WhJ4ZIl", | ||||
|         "days_until_due": null, | ||||
|         "discount": null, | ||||
|         "ended_at": null, | ||||
|         "id": "sub_Do40s9CYZB3Ib8", | ||||
|         "items": { | ||||
|           "data": [ | ||||
|             { | ||||
|               "created": 1539832159, | ||||
|               "id": "si_Do40INfOLVpONR", | ||||
|               "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_Do40s9CYZB3Ib8" | ||||
|             } | ||||
|           ], | ||||
|           "has_more": false, | ||||
|           "object": "list", | ||||
|           "total_count": 1, | ||||
|           "url": "/v1/subscription_items?subscription=sub_Do40s9CYZB3Ib8" | ||||
|         }, | ||||
|         "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": 1539832158, | ||||
|         "status": "active", | ||||
|         "tax_percent": 0.0, | ||||
|         "trial_end": null, | ||||
|         "trial_start": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_Do40UO0WhJ4ZIl/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| } | ||||
| @@ -0,0 +1,82 @@ | ||||
| { | ||||
|   "application_fee_percent": null, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_cycle_anchor": 1539832158, | ||||
|   "cancel_at_period_end": false, | ||||
|   "canceled_at": null, | ||||
|   "created": 1539832158, | ||||
|   "current_period_end": 1571368158, | ||||
|   "current_period_start": 1539832158, | ||||
|   "customer": "cus_Do40UO0WhJ4ZIl", | ||||
|   "days_until_due": null, | ||||
|   "discount": null, | ||||
|   "ended_at": null, | ||||
|   "id": "sub_Do40s9CYZB3Ib8", | ||||
|   "items": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "created": 1539832159, | ||||
|         "id": "si_Do40INfOLVpONR", | ||||
|         "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_Do40s9CYZB3Ib8" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/subscription_items?subscription=sub_Do40s9CYZB3Ib8" | ||||
|   }, | ||||
|   "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": 1539832158, | ||||
|   "status": "active", | ||||
|   "tax_percent": 0.0, | ||||
|   "trial_end": null, | ||||
|   "trial_start": null | ||||
| } | ||||
| @@ -14,18 +14,18 @@ | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000001", | ||||
|     "fingerprint": "6dAXT9VZvwro65EK", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000001", | ||||
|     "id": "card_1DMRsvGh0CmXqmnwRFN7yRRD", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "client_ip": "0.0.0.0", | ||||
|   "created": 1000000000, | ||||
|   "id": "tok_NORMALIZED00000000000001", | ||||
|   "client_ip": "107.202.144.213", | ||||
|   "created": 1539832157, | ||||
|   "id": "tok_1DMRsvGh0CmXqmnwO80yZKru", | ||||
|   "livemode": false, | ||||
|   "object": "token", | ||||
|   "type": "card", | ||||
| @@ -1,78 +0,0 @@ | ||||
| { | ||||
|   "amount": 48000, | ||||
|   "amount_refunded": 0, | ||||
|   "application": null, | ||||
|   "application_fee": null, | ||||
|   "application_fee_amount": null, | ||||
|   "balance_transaction": "txn_NORMALIZED00000000000001", | ||||
|   "captured": true, | ||||
|   "created": 1000000000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "description": "Upgrade to Zulip Standard, $80.0 x 6", | ||||
|   "destination": null, | ||||
|   "dispute": null, | ||||
|   "failure_code": null, | ||||
|   "failure_message": null, | ||||
|   "fraud_details": {}, | ||||
|   "id": "ch_NORMALIZED00000000000001", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "charge", | ||||
|   "on_behalf_of": null, | ||||
|   "order": null, | ||||
|   "outcome": { | ||||
|     "network_status": "approved_by_network", | ||||
|     "reason": null, | ||||
|     "risk_level": "normal", | ||||
|     "risk_score": 00, | ||||
|     "seller_message": "Payment complete.", | ||||
|     "type": "authorized" | ||||
|   }, | ||||
|   "paid": true, | ||||
|   "payment_intent": null, | ||||
|   "receipt_email": "hamlet@zulip.com", | ||||
|   "receipt_number": null, | ||||
|   "receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000001/rcpt_NORMALIZED000000000000000000001", | ||||
|   "refunded": false, | ||||
|   "refunds": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/charges/ch_NORMALIZED00000000000001/refunds" | ||||
|   }, | ||||
|   "review": null, | ||||
|   "shipping": null, | ||||
|   "source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_NORMALIZED0001", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000001", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000001", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "source_transfer": null, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "succeeded", | ||||
|   "transfer_data": null, | ||||
|   "transfer_group": null | ||||
| } | ||||
| @@ -1,92 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 0, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 0, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "auto_advance": true, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": null, | ||||
|   "ending_balance": null, | ||||
|   "finalized_at": null, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": null, | ||||
|   "id": "in_NORMALIZED00000000000001", | ||||
|   "invoice_pdf": null, | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 48000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000001", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1357095845, | ||||
|           "start": 1325473445 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 6, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": -48000, | ||||
|         "currency": "usd", | ||||
|         "description": "Payment (Card ending in 4242)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000002", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 2, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": 1000000000, | ||||
|   "number": "NORMALI-0001", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "draft", | ||||
|   "subscription": null, | ||||
|   "subtotal": 0, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 0, | ||||
|   "webhooks_delivered_at": null | ||||
| } | ||||
| @@ -1,112 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 80697, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 80697, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "auto_advance": true, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": null, | ||||
|   "ending_balance": null, | ||||
|   "finalized_at": null, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": null, | ||||
|   "id": "in_NORMALIZED00000000000002", | ||||
|   "invoice_pdf": null, | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 7255, | ||||
|         "currency": "usd", | ||||
|         "description": "Additional license (Feb 5, 2013 - Jan 2, 2014)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000003", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000003", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1388631845, | ||||
|           "start": 1360033445 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": 56000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard - renewal", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000004", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000004", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1388631845, | ||||
|           "start": 1357095845 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 7, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": 17442, | ||||
|         "currency": "usd", | ||||
|         "description": "Additional license (Apr 11, 2012 - Jan 2, 2013)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000005", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000005", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1357095845, | ||||
|           "start": 1334113445 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 3, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 3, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": 1000000000, | ||||
|   "number": "NORMALI-0002", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "draft", | ||||
|   "subscription": null, | ||||
|   "subtotal": 80697, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 80697, | ||||
|   "webhooks_delivered_at": null | ||||
| } | ||||
| @@ -1,92 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 0, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 0, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": true, | ||||
|   "auto_advance": false, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": 1000000000, | ||||
|   "ending_balance": 0, | ||||
|   "finalized_at": 1000000000, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001", | ||||
|   "id": "in_NORMALIZED00000000000001", | ||||
|   "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf", | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 48000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000001", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1357095845, | ||||
|           "start": 1325473445 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 6, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": -48000, | ||||
|         "currency": "usd", | ||||
|         "description": "Payment (Card ending in 4242)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000002", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1000000000, | ||||
|           "start": 1000000000 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 2, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": null, | ||||
|   "number": "NORMALI-0001", | ||||
|   "object": "invoice", | ||||
|   "paid": true, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "paid", | ||||
|   "subscription": null, | ||||
|   "subtotal": 0, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 0, | ||||
|   "webhooks_delivered_at": 1000000000 | ||||
| } | ||||
| @@ -1,112 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 80697, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 80697, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "auto_advance": true, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": 1000000000, | ||||
|   "ending_balance": 0, | ||||
|   "finalized_at": 1000000000, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002", | ||||
|   "id": "in_NORMALIZED00000000000002", | ||||
|   "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002/pdf", | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 7255, | ||||
|         "currency": "usd", | ||||
|         "description": "Additional license (Feb 5, 2013 - Jan 2, 2014)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000003", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000003", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1388631845, | ||||
|           "start": 1360033445 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": 56000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard - renewal", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000004", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000004", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1388631845, | ||||
|           "start": 1357095845 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 7, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": 17442, | ||||
|         "currency": "usd", | ||||
|         "description": "Additional license (Apr 11, 2012 - Jan 2, 2013)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_NORMALIZED00000000000005", | ||||
|         "invoice_item": "ii_NORMALIZED00000000000005", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1357095845, | ||||
|           "start": 1334113445 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 3, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 3, | ||||
|     "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": 1000000000, | ||||
|   "number": "NORMALI-0002", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1000000000, | ||||
|   "period_start": 1000000000, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "open", | ||||
|   "subscription": null, | ||||
|   "subtotal": 80697, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 80697, | ||||
|   "webhooks_delivered_at": 1000000000 | ||||
| } | ||||
| @@ -1,211 +0,0 @@ | ||||
| { | ||||
|   "data": [ | ||||
|     { | ||||
|       "amount_due": 80697, | ||||
|       "amount_paid": 0, | ||||
|       "amount_remaining": 80697, | ||||
|       "application_fee": null, | ||||
|       "attempt_count": 0, | ||||
|       "attempted": false, | ||||
|       "auto_advance": true, | ||||
|       "billing": "charge_automatically", | ||||
|       "billing_reason": "manual", | ||||
|       "charge": null, | ||||
|       "currency": "usd", | ||||
|       "custom_fields": null, | ||||
|       "customer": "cus_NORMALIZED0001", | ||||
|       "date": 1000000000, | ||||
|       "default_source": null, | ||||
|       "description": "", | ||||
|       "discount": null, | ||||
|       "due_date": 1000000000, | ||||
|       "ending_balance": 0, | ||||
|       "finalized_at": 1000000000, | ||||
|       "footer": null, | ||||
|       "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002", | ||||
|       "id": "in_NORMALIZED00000000000002", | ||||
|       "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000002/pdf", | ||||
|       "lines": { | ||||
|         "data": [ | ||||
|           { | ||||
|             "amount": 7255, | ||||
|             "currency": "usd", | ||||
|             "description": "Additional license (Feb 5, 2013 - Jan 2, 2014)", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000003", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000003", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1388631845, | ||||
|               "start": 1360033445 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 1, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           }, | ||||
|           { | ||||
|             "amount": 56000, | ||||
|             "currency": "usd", | ||||
|             "description": "Zulip Standard - renewal", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000004", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000004", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1388631845, | ||||
|               "start": 1357095845 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 7, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           }, | ||||
|           { | ||||
|             "amount": 17442, | ||||
|             "currency": "usd", | ||||
|             "description": "Additional license (Apr 11, 2012 - Jan 2, 2013)", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000005", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000005", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1357095845, | ||||
|               "start": 1334113445 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 3, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           } | ||||
|         ], | ||||
|         "has_more": false, | ||||
|         "object": "list", | ||||
|         "total_count": 3, | ||||
|         "url": "/v1/invoices/in_NORMALIZED00000000000002/lines" | ||||
|       }, | ||||
|       "livemode": false, | ||||
|       "metadata": {}, | ||||
|       "next_payment_attempt": 1000000000, | ||||
|       "number": "NORMALI-0002", | ||||
|       "object": "invoice", | ||||
|       "paid": false, | ||||
|       "period_end": 1000000000, | ||||
|       "period_start": 1000000000, | ||||
|       "receipt_number": null, | ||||
|       "starting_balance": 0, | ||||
|       "statement_descriptor": "Zulip Standard", | ||||
|       "status": "open", | ||||
|       "subscription": null, | ||||
|       "subtotal": 80697, | ||||
|       "tax": 0, | ||||
|       "tax_percent": null, | ||||
|       "total": 80697, | ||||
|       "webhooks_delivered_at": 1000000000 | ||||
|     }, | ||||
|     { | ||||
|       "amount_due": 0, | ||||
|       "amount_paid": 0, | ||||
|       "amount_remaining": 0, | ||||
|       "application_fee": null, | ||||
|       "attempt_count": 0, | ||||
|       "attempted": true, | ||||
|       "auto_advance": false, | ||||
|       "billing": "charge_automatically", | ||||
|       "billing_reason": "manual", | ||||
|       "charge": null, | ||||
|       "currency": "usd", | ||||
|       "custom_fields": null, | ||||
|       "customer": "cus_NORMALIZED0001", | ||||
|       "date": 1000000000, | ||||
|       "default_source": null, | ||||
|       "description": "", | ||||
|       "discount": null, | ||||
|       "due_date": 1000000000, | ||||
|       "ending_balance": 0, | ||||
|       "finalized_at": 1000000000, | ||||
|       "footer": null, | ||||
|       "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001", | ||||
|       "id": "in_NORMALIZED00000000000001", | ||||
|       "invoice_pdf": "https://pay.stripe.com/invoice/invst_NORMALIZED0000000000000001/pdf", | ||||
|       "lines": { | ||||
|         "data": [ | ||||
|           { | ||||
|             "amount": 48000, | ||||
|             "currency": "usd", | ||||
|             "description": "Zulip Standard", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000001", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000001", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1357095845, | ||||
|               "start": 1325473445 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 6, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           }, | ||||
|           { | ||||
|             "amount": -48000, | ||||
|             "currency": "usd", | ||||
|             "description": "Payment (Card ending in 4242)", | ||||
|             "discountable": false, | ||||
|             "id": "ii_NORMALIZED00000000000002", | ||||
|             "invoice_item": "ii_NORMALIZED00000000000002", | ||||
|             "livemode": false, | ||||
|             "metadata": {}, | ||||
|             "object": "line_item", | ||||
|             "period": { | ||||
|               "end": 1000000000, | ||||
|               "start": 1000000000 | ||||
|             }, | ||||
|             "plan": null, | ||||
|             "proration": false, | ||||
|             "quantity": 1, | ||||
|             "subscription": null, | ||||
|             "type": "invoiceitem" | ||||
|           } | ||||
|         ], | ||||
|         "has_more": false, | ||||
|         "object": "list", | ||||
|         "total_count": 2, | ||||
|         "url": "/v1/invoices/in_NORMALIZED00000000000001/lines" | ||||
|       }, | ||||
|       "livemode": false, | ||||
|       "metadata": {}, | ||||
|       "next_payment_attempt": null, | ||||
|       "number": "NORMALI-0001", | ||||
|       "object": "invoice", | ||||
|       "paid": true, | ||||
|       "period_end": 1000000000, | ||||
|       "period_start": 1000000000, | ||||
|       "receipt_number": null, | ||||
|       "starting_balance": 0, | ||||
|       "statement_descriptor": "Zulip Standard", | ||||
|       "status": "paid", | ||||
|       "subscription": null, | ||||
|       "subtotal": 0, | ||||
|       "tax": 0, | ||||
|       "tax_percent": null, | ||||
|       "total": 0, | ||||
|       "webhooks_delivered_at": 1000000000 | ||||
|     } | ||||
|   ], | ||||
|   "has_more": false, | ||||
|   "object": "list", | ||||
|   "url": "/v1/invoices" | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": -48000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Payment (Card ending in 4242)", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000002", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1000000000, | ||||
|     "start": 1000000000 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 1, | ||||
|   "subscription": null, | ||||
|   "unit_amount": -48000 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": 48000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Zulip Standard", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000001", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1357095845, | ||||
|     "start": 1325473445 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 6, | ||||
|   "subscription": null, | ||||
|   "unit_amount": 8000 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": 17442, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Additional license (Apr 11, 2012 - Jan 2, 2013)", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000005", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1357095845, | ||||
|     "start": 1334113445 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 3, | ||||
|   "subscription": null, | ||||
|   "unit_amount": 5814 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": 56000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Zulip Standard - renewal", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000004", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1388631845, | ||||
|     "start": 1357095845 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 7, | ||||
|   "subscription": null, | ||||
|   "unit_amount": 8000 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": 7255, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "date": 1000000000, | ||||
|   "description": "Additional license (Feb 5, 2013 - Jan 2, 2014)", | ||||
|   "discountable": false, | ||||
|   "id": "ii_NORMALIZED00000000000003", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1388631845, | ||||
|     "start": 1360033445 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 1, | ||||
|   "subscription": null, | ||||
|   "unit_amount": 7255 | ||||
| } | ||||
| @@ -1,78 +0,0 @@ | ||||
| { | ||||
|   "amount": 64000, | ||||
|   "amount_refunded": 0, | ||||
|   "application": null, | ||||
|   "application_fee": null, | ||||
|   "application_fee_amount": null, | ||||
|   "balance_transaction": "txn_1DxdeSGh0CmXqmnw9I7ziL6D", | ||||
|   "captured": true, | ||||
|   "created": 1548695524, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_EQUd48LphR5ahk", | ||||
|   "description": "Upgrade to Zulip Standard, $80.0 x 8", | ||||
|   "destination": null, | ||||
|   "dispute": null, | ||||
|   "failure_code": null, | ||||
|   "failure_message": null, | ||||
|   "fraud_details": {}, | ||||
|   "id": "ch_1DxdeSGh0CmXqmnwsw80Rn8n", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "charge", | ||||
|   "on_behalf_of": null, | ||||
|   "order": null, | ||||
|   "outcome": { | ||||
|     "network_status": "approved_by_network", | ||||
|     "reason": null, | ||||
|     "risk_level": "normal", | ||||
|     "risk_score": 46, | ||||
|     "seller_message": "Payment complete.", | ||||
|     "type": "authorized" | ||||
|   }, | ||||
|   "paid": true, | ||||
|   "payment_intent": null, | ||||
|   "receipt_email": "hamlet@zulip.com", | ||||
|   "receipt_number": null, | ||||
|   "receipt_url": "https://pay.stripe.com/receipts/acct_1BWYgHGh0CmXqmnw/ch_1DxdeSGh0CmXqmnwsw80Rn8n/rcpt_EQUda9O5I5cYpJL0yxiRbxnPbWZ415D", | ||||
|   "refunded": false, | ||||
|   "refunds": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/charges/ch_1DxdeSGh0CmXqmnwsw80Rn8n/refunds" | ||||
|   }, | ||||
|   "review": null, | ||||
|   "shipping": null, | ||||
|   "source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_EQUd48LphR5ahk", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "6dAXT9VZvwro65EK", | ||||
|     "funding": "credit", | ||||
|     "id": "card_1DxdeRGh0CmXqmnwu3BibWtM", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "source_transfer": null, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "succeeded", | ||||
|   "transfer_data": null, | ||||
|   "transfer_group": null | ||||
| } | ||||
| @@ -1,92 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 0, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 0, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": false, | ||||
|   "auto_advance": true, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_EQUd48LphR5ahk", | ||||
|   "date": 1548695526, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": null, | ||||
|   "ending_balance": null, | ||||
|   "finalized_at": null, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": null, | ||||
|   "id": "in_1DxdeUGh0CmXqmnwoowtAcQW", | ||||
|   "invoice_pdf": null, | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 64000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_1DxdeTGh0CmXqmnw7MfZoBVx", | ||||
|         "invoice_item": "ii_1DxdeTGh0CmXqmnw7MfZoBVx", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1357095845, | ||||
|           "start": 1325473445 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 8, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": -64000, | ||||
|         "currency": "usd", | ||||
|         "description": "Payment (Card ending in 4242)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_1DxdeTGh0CmXqmnwDYYlalDE", | ||||
|         "invoice_item": "ii_1DxdeTGh0CmXqmnwDYYlalDE", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1548695525, | ||||
|           "start": 1548695525 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 2, | ||||
|     "url": "/v1/invoices/in_1DxdeUGh0CmXqmnwoowtAcQW/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": 1548699126, | ||||
|   "number": "9F53235-0001", | ||||
|   "object": "invoice", | ||||
|   "paid": false, | ||||
|   "period_end": 1548695526, | ||||
|   "period_start": 1548695526, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "draft", | ||||
|   "subscription": null, | ||||
|   "subtotal": 0, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 0, | ||||
|   "webhooks_delivered_at": null | ||||
| } | ||||
| @@ -1,92 +0,0 @@ | ||||
| { | ||||
|   "amount_due": 0, | ||||
|   "amount_paid": 0, | ||||
|   "amount_remaining": 0, | ||||
|   "application_fee": null, | ||||
|   "attempt_count": 0, | ||||
|   "attempted": true, | ||||
|   "auto_advance": false, | ||||
|   "billing": "charge_automatically", | ||||
|   "billing_reason": "manual", | ||||
|   "charge": null, | ||||
|   "currency": "usd", | ||||
|   "custom_fields": null, | ||||
|   "customer": "cus_EQUd48LphR5ahk", | ||||
|   "date": 1548695526, | ||||
|   "default_source": null, | ||||
|   "description": "", | ||||
|   "discount": null, | ||||
|   "due_date": 1551287527, | ||||
|   "ending_balance": 0, | ||||
|   "finalized_at": 1548695527, | ||||
|   "footer": null, | ||||
|   "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_8N3Zw202aBfkZSHzyjUVcv9vrE", | ||||
|   "id": "in_1DxdeUGh0CmXqmnwoowtAcQW", | ||||
|   "invoice_pdf": "https://pay.stripe.com/invoice/invst_8N3Zw202aBfkZSHzyjUVcv9vrE/pdf", | ||||
|   "lines": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "amount": 64000, | ||||
|         "currency": "usd", | ||||
|         "description": "Zulip Standard", | ||||
|         "discountable": false, | ||||
|         "id": "ii_1DxdeTGh0CmXqmnw7MfZoBVx", | ||||
|         "invoice_item": "ii_1DxdeTGh0CmXqmnw7MfZoBVx", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1357095845, | ||||
|           "start": 1325473445 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 8, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       }, | ||||
|       { | ||||
|         "amount": -64000, | ||||
|         "currency": "usd", | ||||
|         "description": "Payment (Card ending in 4242)", | ||||
|         "discountable": false, | ||||
|         "id": "ii_1DxdeTGh0CmXqmnwDYYlalDE", | ||||
|         "invoice_item": "ii_1DxdeTGh0CmXqmnwDYYlalDE", | ||||
|         "livemode": false, | ||||
|         "metadata": {}, | ||||
|         "object": "line_item", | ||||
|         "period": { | ||||
|           "end": 1548695525, | ||||
|           "start": 1548695525 | ||||
|         }, | ||||
|         "plan": null, | ||||
|         "proration": false, | ||||
|         "quantity": 1, | ||||
|         "subscription": null, | ||||
|         "type": "invoiceitem" | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 2, | ||||
|     "url": "/v1/invoices/in_1DxdeUGh0CmXqmnwoowtAcQW/lines" | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "next_payment_attempt": null, | ||||
|   "number": "9F53235-0001", | ||||
|   "object": "invoice", | ||||
|   "paid": true, | ||||
|   "period_end": 1548695526, | ||||
|   "period_start": 1548695526, | ||||
|   "receipt_number": null, | ||||
|   "starting_balance": 0, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "paid", | ||||
|   "subscription": null, | ||||
|   "subtotal": 0, | ||||
|   "tax": 0, | ||||
|   "tax_percent": null, | ||||
|   "total": 0, | ||||
|   "webhooks_delivered_at": 1548695526 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": -64000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_EQUd48LphR5ahk", | ||||
|   "date": 1548695525, | ||||
|   "description": "Payment (Card ending in 4242)", | ||||
|   "discountable": false, | ||||
|   "id": "ii_1DxdeTGh0CmXqmnwDYYlalDE", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1548695525, | ||||
|     "start": 1548695525 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 1, | ||||
|   "subscription": null, | ||||
|   "unit_amount": -64000 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": 64000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_EQUd48LphR5ahk", | ||||
|   "date": 1548695525, | ||||
|   "description": "Zulip Standard", | ||||
|   "discountable": false, | ||||
|   "id": "ii_1DxdeTGh0CmXqmnw7MfZoBVx", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1357095845, | ||||
|     "start": 1325473445 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 8, | ||||
|   "subscription": null, | ||||
|   "unit_amount": 8000 | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "amount": 17442, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_EQUd48LphR5ahk", | ||||
|   "date": 1548695527, | ||||
|   "description": "Additional license (Apr 11, 2012 - Jan 2, 2013)", | ||||
|   "discountable": false, | ||||
|   "id": "ii_1DxdeVGh0CmXqmnwf6cLlEA7", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "invoiceitem", | ||||
|   "period": { | ||||
|     "end": 1357095845, | ||||
|     "start": 1334113445 | ||||
|   }, | ||||
|   "plan": null, | ||||
|   "proration": false, | ||||
|   "quantity": 3, | ||||
|   "subscription": null, | ||||
|   "unit_amount": 5814 | ||||
| } | ||||
| @@ -1,78 +0,0 @@ | ||||
| { | ||||
|   "amount": 48000, | ||||
|   "amount_refunded": 0, | ||||
|   "application": null, | ||||
|   "application_fee": null, | ||||
|   "application_fee_amount": null, | ||||
|   "balance_transaction": "txn_NORMALIZED00000000000001", | ||||
|   "captured": true, | ||||
|   "created": 1000000000, | ||||
|   "currency": "usd", | ||||
|   "customer": "cus_NORMALIZED0001", | ||||
|   "description": "Upgrade to Zulip Standard, $80.0 x 6", | ||||
|   "destination": null, | ||||
|   "dispute": null, | ||||
|   "failure_code": null, | ||||
|   "failure_message": null, | ||||
|   "fraud_details": {}, | ||||
|   "id": "ch_NORMALIZED00000000000001", | ||||
|   "invoice": null, | ||||
|   "livemode": false, | ||||
|   "metadata": {}, | ||||
|   "object": "charge", | ||||
|   "on_behalf_of": null, | ||||
|   "order": null, | ||||
|   "outcome": { | ||||
|     "network_status": "approved_by_network", | ||||
|     "reason": null, | ||||
|     "risk_level": "normal", | ||||
|     "risk_score": 00, | ||||
|     "seller_message": "Payment complete.", | ||||
|     "type": "authorized" | ||||
|   }, | ||||
|   "paid": true, | ||||
|   "payment_intent": null, | ||||
|   "receipt_email": "hamlet@zulip.com", | ||||
|   "receipt_number": null, | ||||
|   "receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000001/rcpt_NORMALIZED000000000000000000001", | ||||
|   "refunded": false, | ||||
|   "refunds": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/charges/ch_NORMALIZED00000000000001/refunds" | ||||
|   }, | ||||
|   "review": null, | ||||
|   "shipping": null, | ||||
|   "source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_NORMALIZED0001", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000001", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000001", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "source_transfer": null, | ||||
|   "statement_descriptor": "Zulip Standard", | ||||
|   "status": "succeeded", | ||||
|   "transfer_data": null, | ||||
|   "transfer_group": null | ||||
| } | ||||
| @@ -1,65 +0,0 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1000000000, | ||||
|   "currency": null, | ||||
|   "default_source": "card_NORMALIZED00000000000001", | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_NORMALIZED0001", | ||||
|   "invoice_prefix": "NORMA01", | ||||
|   "invoice_settings": { | ||||
|     "custom_fields": null, | ||||
|     "footer": null | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
|     "realm_str": "zulip" | ||||
|   }, | ||||
|   "object": "customer", | ||||
|   "shipping": null, | ||||
|   "sources": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_NORMALIZED0001", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "NORMALIZED000001", | ||||
|         "funding": "credit", | ||||
|         "id": "card_NORMALIZED00000000000001", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| } | ||||
| @@ -1,89 +0,0 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1000000000, | ||||
|   "currency": "usd", | ||||
|   "default_source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "Visa", | ||||
|     "country": "US", | ||||
|     "customer": "cus_NORMALIZED0001", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000001", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000001", | ||||
|     "last4": "4242", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_NORMALIZED0001", | ||||
|   "invoice_prefix": "NORMA01", | ||||
|   "invoice_settings": { | ||||
|     "custom_fields": null, | ||||
|     "footer": null | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
|     "realm_str": "zulip" | ||||
|   }, | ||||
|   "object": "customer", | ||||
|   "shipping": null, | ||||
|   "sources": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "Visa", | ||||
|         "country": "US", | ||||
|         "customer": "cus_NORMALIZED0001", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "NORMALIZED000001", | ||||
|         "funding": "credit", | ||||
|         "id": "card_NORMALIZED00000000000001", | ||||
|         "last4": "4242", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| } | ||||
| @@ -1,89 +0,0 @@ | ||||
| { | ||||
|   "account_balance": 0, | ||||
|   "created": 1000000000, | ||||
|   "currency": "usd", | ||||
|   "default_source": { | ||||
|     "address_city": "Pacific", | ||||
|     "address_country": "United States", | ||||
|     "address_line1": "Under the sea,", | ||||
|     "address_line1_check": "pass", | ||||
|     "address_line2": null, | ||||
|     "address_state": null, | ||||
|     "address_zip": "33333", | ||||
|     "address_zip_check": "pass", | ||||
|     "brand": "MasterCard", | ||||
|     "country": "US", | ||||
|     "customer": "cus_NORMALIZED0001", | ||||
|     "cvc_check": "pass", | ||||
|     "dynamic_last4": null, | ||||
|     "exp_month": 3, | ||||
|     "exp_year": 2033, | ||||
|     "fingerprint": "NORMALIZED000002", | ||||
|     "funding": "credit", | ||||
|     "id": "card_NORMALIZED00000000000002", | ||||
|     "last4": "4444", | ||||
|     "metadata": {}, | ||||
|     "name": "Ada Starr", | ||||
|     "object": "card", | ||||
|     "tokenization_method": null | ||||
|   }, | ||||
|   "delinquent": false, | ||||
|   "description": "zulip (Zulip Dev)", | ||||
|   "discount": null, | ||||
|   "email": "hamlet@zulip.com", | ||||
|   "id": "cus_NORMALIZED0001", | ||||
|   "invoice_prefix": "NORMA01", | ||||
|   "invoice_settings": { | ||||
|     "custom_fields": null, | ||||
|     "footer": null | ||||
|   }, | ||||
|   "livemode": false, | ||||
|   "metadata": { | ||||
|     "realm_id": "1", | ||||
|     "realm_str": "zulip" | ||||
|   }, | ||||
|   "object": "customer", | ||||
|   "shipping": null, | ||||
|   "sources": { | ||||
|     "data": [ | ||||
|       { | ||||
|         "address_city": "Pacific", | ||||
|         "address_country": "United States", | ||||
|         "address_line1": "Under the sea,", | ||||
|         "address_line1_check": "pass", | ||||
|         "address_line2": null, | ||||
|         "address_state": null, | ||||
|         "address_zip": "33333", | ||||
|         "address_zip_check": "pass", | ||||
|         "brand": "MasterCard", | ||||
|         "country": "US", | ||||
|         "customer": "cus_NORMALIZED0001", | ||||
|         "cvc_check": "pass", | ||||
|         "dynamic_last4": null, | ||||
|         "exp_month": 3, | ||||
|         "exp_year": 2033, | ||||
|         "fingerprint": "NORMALIZED000002", | ||||
|         "funding": "credit", | ||||
|         "id": "card_NORMALIZED00000000000002", | ||||
|         "last4": "4444", | ||||
|         "metadata": {}, | ||||
|         "name": "Ada Starr", | ||||
|         "object": "card", | ||||
|         "tokenization_method": null | ||||
|       } | ||||
|     ], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 1, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/sources" | ||||
|   }, | ||||
|   "subscriptions": { | ||||
|     "data": [], | ||||
|     "has_more": false, | ||||
|     "object": "list", | ||||
|     "total_count": 0, | ||||
|     "url": "/v1/customers/cus_NORMALIZED0001/subscriptions" | ||||
|   }, | ||||
|   "tax_info": null, | ||||
|   "tax_info_verification": null | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user