Compare commits

..

53 Commits

Author SHA1 Message Date
Tim Abbott
8e7ac21fe0 Release Zulip Server 1.9.2. 2019-01-29 16:33:36 -08:00
Tim Abbott
cbfae3e0d0 import: Fix uploading avatars with S3 upload backend.
This should hopefully be the last commit of this form; ultimately, my
hope is that we'll be able to refactor the semi-duplicated logic in
this file to avoid so much effort going into keeping this correct.
2019-01-29 16:26:19 -08:00
Tim Abbott
ffeb4340a9 auth: Migrate Google authentication off deprecated name API.
As part of Google+ being removed, they've eliminated support for the
/plus/v1/people/me endpoint.  Replace it with the very similar
/oauth2/v3/userinfo endpoint.
2019-01-29 16:17:27 -08:00
Matthew Wegner
79f781b9ea import: Normalize Slackbot String Comparison.
In very old Slack workspaces, slackbot can appear as "Slackbot", and
the import script only checks for "slackbot" (case sensitive).  This
breaks the import--it throws the assert that immediately follows the
test.  I don't know how common this is, but it definitely affected our
import.

The simple fix is to compare against a lowercased-version of the
user's full name.
2019-01-29 16:14:10 -08:00
Tim Abbott
fd89df63b4 hipchat: Fix importing of private messages.
Apparently a stupid typing issue meant that we broke this a few weeks
ago.
2019-01-29 16:13:25 -08:00
Tim Abbott
509d335705 import: Handle corner case around EMAIL_GATEWAY_BOT emails. 2019-01-29 16:12:01 -08:00
Tim Abbott
ce28ccf2bf import: Fix pointer logic for zulip->zulip imports.
Previously, the pointer was almost guaranteed to be an invalid random
value, because we renumber message IDs unconditionally now.
2019-01-29 16:11:56 -08:00
Tim Abbott
4adbeedef6 hipchat: Handle unusual emoticons.json format.
Apparently, hc-migrate can generate emoticons.json files with a
somewhat different format.  Assuming that other files are in the
normal format, we should be able to handle it like this.

See report in #11135.
2019-01-29 16:11:34 -08:00
Tim Abbott
09ed7d5b77 hipchat: Handle case where emoticons.json is not in export.
Apparently, some methods of exporting from HipChat do not include an
emoticons.json file.  We could test for this using the
`include_emoticons` field in `metadata.json`, but we currently don't
even bother to read that file.  Rather than changing that, we just
print a warning and proceed.  This is arguably better anyway, in that
often not having emoticons.json is the result of user error when
exporting, and it's nice to flag that this is happening.

Fixes #11135.
2019-01-29 16:11:30 -08:00
Tim Abbott
f445d3f589 import: Ensure presence of basic avatar images for HipChat.
Our HipChat conversion tool didn't properly handle basic avatar
images, resulting in only the medium-size avatar images being imported
properly.  This fixes that bug by asking the import tool to do the
thumbnailing for the basic avatar image (from the .original file) as
well as the medium avatar image.
2019-01-29 16:11:23 -08:00
Tim Abbott
e8ee374d4f slack import: Import long-inactive users as long-term idle.
This avoids creating UserMessage rows for long-inactive users in
organizations with many thousands of users.
2019-01-29 16:10:59 -08:00
Tim Abbott
9ff5359522 export: Remove assertion on current working directory.
This command hasn't made deep assumptions about CWD for a long time,
and this enables users to run it through a symlink (etc.).

Fixes #10961.
2019-01-29 16:10:26 -08:00
Tim Abbott
a31f56443a import: Avoid unnecessary forks when downloading attachments.
The previous implementation used run_parallel incorrectly, passing it
a set of very small jobs (each was to download a single file), which
meant that we'd end up forking once for every file to download.

This correct implementation sends each of N threads 1/N of the files
to download, which is more consistent with the goal of distributing
the download work between N threads.
2019-01-29 16:09:42 -08:00
rht
0b263d8b8c slack import: Eliminate need to load all messages into memory.
This works by yielding messages sorted based on timestamp.  Because
the Slack exports are broken into files by date, it's convenient to do
a 2-layer sorting process, where we open all the files for a given
day, and then sort their messages by timestamp before yielding them.

Fixes #10930.
2019-01-29 16:09:37 -08:00
Tim Abbott
ad00b02c66 slack import: Fix all messages being imported to one channel.
This was an ugly variable-escape-from-loop regression introduced in
e59ff6e6db.
2019-01-29 16:09:06 -08:00
Tim Abbott
02f2ae4048 slack import: Fix empty values for custom profile fields.
The Slack import process would incorrectly issue
CustomProfileFieldValue entries with a value of "" for users who
didn't have a given CustomProfileField (especially common for the
"skype" and "phone" fields).  This had no user-visible effect, but
certainly added some clutter in the database.
2019-01-29 16:09:02 -08:00
Tim Abbott
56d4426738 gitter: Do something reasonable with invalid fullnames. 2019-01-29 16:08:55 -08:00
Tim Abbott
f0fe7d3887 scripts: Recommend apt update after enabling universe.
One needs to manually do an apt update after add-apt-repository, or it
won't actually work.
2019-01-29 16:07:31 -08:00
Sumanth V Rao
21166fbdf9 upgrade-zulip-stage-2: Added argument to skip purging old deployments.
This makes it possible to add --skip-purge-old-deployments in the
deploy_options section of /etc/zulip/zulip.conf, and control whether
old deployments are purged automatically on a system.

We still need to do https://github.com/zulip/zulip/issues/10534 and
probably also to add these arguments to be directly passed into
upgrade-zulip, but that can wait for future work.

Fixes #10946.
2019-01-29 16:06:08 -08:00
Tim Abbott
b2c865aab5 scripts: Fix incorrect garbage-collection of emoji/node caches.
Apparently, we were incorrectly expressing the paths in the
caches_in_use data structures for these two cache-cleaning algorithms,
resulting in the default threshhold_days algorithm controlling which
caches could be garbage-collected.  While the emoji one was just a
performance optimization for upgrade-zulip-from-git, it was possible
for the main `node_modules` cache in use in production to be GCed,
resulting in LaTeX rendering being broken.
2019-01-29 16:05:32 -08:00
Tim Abbott
fd9847ffcb Release Zulip Server 1.9.1. 2018-11-30 13:10:54 -08:00
Tim Abbott
2bca1d4ef0 docs: Move generic reverse proxy notes further down. 2018-11-30 13:08:34 -08:00
Bruce
871fdddf86 docs: Document how to use Zulip behind an haproxy reverse proxy.
With significant rearrangement by tabbott to have more common text
between different proxy implementations.
2018-11-30 13:08:34 -08:00
Igor Posledov
bf4e01eb02 docs: Add nginx reverse proxy basic config example. 2018-11-30 13:08:34 -08:00
Tim Abbott
92ea9a0729 show_admins: Rewrite to use management library.
This makes this command more standardized, and helps avoid future bugs
like the one fixed in the last commit.
2018-11-30 13:08:34 -08:00
Tim Abbott
a36cb9b247 show_admins: Fix buggy realm parsing. 2018-11-30 13:08:34 -08:00
Rohitt Vashishtha
da71f67f85 scripts: Cleanly exit manage.py when run with python2.
Fixes #10854.
2018-11-30 13:08:34 -08:00
Anders Kaseorg
5518abe1d6 install: Check whether universe repository is enabled on Ubuntu.
Fixes #10417.

Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2018-11-30 13:08:33 -08:00
Rohitt Vashishtha
b34848447e scripts: Make upgrade-zulip-* use root checking from zulip_tools.
This is mostly just a nice code deduplication/cleanup.
2018-11-30 13:08:33 -08:00
Rohitt Vashishtha
3d5c994a32 scripts: Make manage.py use root checking from zulip_tools. 2018-11-30 13:08:33 -08:00
Rohitt Vashishtha
cfa2c6bf37 scripts: Make zulip-puppet-apply check if the user is root.
Fixes #10833.
2018-11-30 13:08:33 -08:00
Rohitt Vashishtha
3d243a457f scripts: Add util functions for checking root to zulip_tools. 2018-11-30 13:08:33 -08:00
Tim Abbott
e414036eb3 docs: Fix missing quotes in su zulip -c documentation.
This fixes an actual user-facing issue in our mobile push
notifications documentation (where we were incorrectly failing to
quote the argument to `./manage.py register_server` making it not
work), as well as preventing future similar issues from occurring
again via a linter rule.
2018-11-30 13:08:33 -08:00
Tim Abbott
9ec154bbe8 docs: Document how to setup system postfix email with Zulip. 2018-11-30 13:08:33 -08:00
Tim Abbott
da479c3613 setup-apt-repo: Install gnupg as part of installation.
Apparently, on Debian stretch, the gnupg package isn't installed by
default, which means that our `apt-key add` commands were failing with
these errors on an ultra-minimal Debian installation:

+ apt-key add ./scripts/setup/packagecloud.asc
E: gnupg, gnupg2 and gnupg1 do not seem to be installed, but one of them is required for this operation
+ apt-key add ./scripts/setup/pgroonga-debian.asc
E: gnupg, gnupg2 and gnupg1 do not seem to be installed, but one of them is required for this operation

Fixes #10480.
2018-11-30 13:08:33 -08:00
Tim Abbott
23d8f6e6b0 i18n: Update translation data from transifex. 2018-11-28 12:48:53 -08:00
Tim Abbott
d929ad0122 upload: Fix ensure_medium_avatar_image for S3 backend.
Previously, it tried to interact with the wrong path for the original
image.
2018-11-28 12:30:24 -08:00
Tim Abbott
429d0f9728 push notifications: Improve logging for missing configuration.
While it could make sense to print these logging statements at WARN
level on server startup, it doesn't make sense to do so on every
message (though it perhaps did make sense to do so before more recent
changes added good ways to discover you forgot to configure push
notifications).

Instead, we now just do a WARN log on queue processor startup, and
then at DEBUG level for individual messages.

Fixes #10894.
2018-11-28 12:30:24 -08:00
Tim Abbott
c905915055 push notifications: Fix a comment typo. 2018-11-28 12:30:24 -08:00
Tim Abbott
b238d0e75e docs: Fix some broken links in security model doc.
Apparently, we haven't been running test-documentation in production
of late.
2018-11-28 12:30:23 -08:00
Tim Abbott
a4ebdac521 docs: Document how to use AWS SIGv4 with boto.
This is required in some AWS regions.

The right long-term fix is to move to boto3 which doesn't have this
problem.

Allows us to downgrade the priority of #9376.
2018-11-28 12:27:18 -08:00
Tim Abbott
bdd6e1fabe docs: Fix accidental repeat bullet #1 in S3 backend documentation.
Due to missing indentation, the numbering was resetting to 1 rather
than continuing to 6.
2018-11-28 12:27:15 -08:00
Vishnu Ks
d120ee25b4 auth: Always force Google to show account chooser.
Fixes #10515
2018-11-16 11:57:12 -08:00
Tim Abbott
14938fbf4c nginx: Fix missing API authentication configuration.
This fixes a bug where our API routes for uploaded files (where we
need to use a consistent URL between session auth and API auth) were
not properly configured to pass through the API authentication headers
(and otherwise provide REST endpoint settings).

In particular, this prevented the Zulip mobile apps from being able to
access authenticated image files using these URLs.
2018-11-16 11:57:05 -08:00
Tim Abbott
8babe17f8f docs: Clarify preparatory process for data import.
You need a Zulip server running the a matching version, and no longer
need to do an upgrade from master before using established import tools.
2018-11-14 17:15:02 -08:00
Tim Abbott
724e1d3002 docs: Link to setup-certbot multiple hostname support. 2018-11-14 13:10:05 -08:00
Rohitt Vashishtha
a474a08195 setup-cerbot: Allow issuing certificates for multiple domains.
This commit allows specifying Subject Alternative Names to issue certs
for multiple domains using certbot. The first name passed to certbot-auto
becomes the common name for the certificate; common name and the other
names are then added to the SAN field. All of these arguments are now
positional. Also read the following for the certbot syntax reference:

https://community.letsencrypt.org/t/how-to-specify-subject-name-on-san/

Fixes #10674.
2018-11-14 13:10:02 -08:00
Tim Abbott
a48bbef766 docs: Clarify push registration for running manage.py correctly.
We've had several users get errors running this because they ran it as
a bash script; fix this my making the command super explicit.
2018-11-14 13:09:58 -08:00
Tim Abbott
71c5632e07 install: Provide a suggestive error message when missing Universe.
By far the dominant cause of errors when installing apt packages is
not having the Universe repository enabled in Ubuntu bionic (this
seems to have started happening a lot recently; I wonder if Ubuntu
changed the defaults for new server installs or something?).

In any case, providing that suggestion in the error output should help
reduce these a lot.
2018-11-12 10:57:07 -08:00
Tim Abbott
498b6e4670 install: Improve some error output for common errors.
This uses `set +x` to hide the `echo` output, and then sets the font
color to red.
2018-11-12 10:57:04 -08:00
Tim Abbott
925ddc28f4 import: Don't assume a last_modified key is present.
This fixes an exception when importing uploaded file data from
Slack/HipChat.
2018-11-08 15:29:49 -08:00
Tim Abbott
3651b8d254 import: Fix buggy handling of avatars in Slack conversion.
This was a pretty nasty error, where we were accidentally accessing
the parent list in this inner loop function.

This appears to have been introduced as a refactoring bug in
7822ef38c2.
2018-11-08 15:29:25 -08:00
Tim Abbott
90d3cbceed docs: Further document tokenized noreply email addresses.
We should still extend email.html to explain the security issue a bit
more clearly, since the article we link to is super long.
2018-11-08 15:29:22 -08:00
1711 changed files with 52973 additions and 84405 deletions

View File

@@ -1,33 +1,35 @@
# 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: # See CircleCI upstream's docs on this config format:
# https://circleci.com/docs/2.0/language-python/ # https://circleci.com/docs/2.0/language-python/
# #
version: 2 version: 2
aliases: jobs:
- &create_cache_directories "trusty-python-3.4":
run: docker:
# This is built from tools/circleci/images/trusty/Dockerfile .
- image: gregprice/circleci:trusty-python-5.test
working_directory: ~/zulip
steps:
- checkout
- run:
name: create cache directories name: create cache directories
command: | command: |
dirs=(/srv/zulip-{npm,venv}-cache) dirs=(/srv/zulip-{npm,venv}-cache)
sudo mkdir -p "${dirs[@]}" sudo mkdir -p "${dirs[@]}"
sudo chown -R circleci "${dirs[@]}" sudo chown -R circleci "${dirs[@]}"
- &restore_cache_package_json - restore_cache:
restore_cache:
keys: keys:
- v1-npm-base.{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} - v1-npm-base.trusty-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
- restore_cache:
- &restore_cache_requirements
restore_cache:
keys: keys:
- v1-venv-base.{{ .Environment.CIRCLE_JOB }}-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }} - v1-venv-base.trusty-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }}
- &install_dependencies - run:
run:
name: install dependencies name: install dependencies
command: | command: |
sudo apt-get update
# Install moreutils so we can use `ts` and `mispipe` in the following. # Install moreutils so we can use `ts` and `mispipe` in the following.
sudo apt-get install -y moreutils sudo apt-get install -y moreutils
@@ -40,68 +42,43 @@ aliases:
rm -f /home/circleci/.gitconfig rm -f /home/circleci/.gitconfig
# This is the main setup job for the test suite # This is the main setup job for the test suite
mispipe "tools/ci/setup-backend" ts mispipe "tools/travis/setup-backend" ts
# Cleaning caches is mostly unnecessary in Circle, because # Cleaning caches is mostly unnecessary in Circle, because
# most builds don't get to write to the cache. # most builds don't get to write to the cache.
# mispipe "scripts/lib/clean-unused-caches --verbose --threshold 0" ts # mispipe "scripts/lib/clean-unused-caches --verbose --threshold 0" ts
- &save_cache_package_json - save_cache:
save_cache:
paths: paths:
- /srv/zulip-npm-cache - /srv/zulip-npm-cache
key: v1-npm-base.{{ .Environment.CIRCLE_JOB }}-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} key: v1-npm-base.trusty-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
- save_cache:
- &save_cache_requirements
save_cache:
paths: paths:
- /srv/zulip-venv-cache - /srv/zulip-venv-cache
key: v1-venv-base.{{ .Environment.CIRCLE_JOB }}-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }} key: v1-venv-base.trusty-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }}
# TODO: in Travis we also cache ~/zulip-emoji-cache, ~/node, ~/misc # TODO: in Travis we also cache ~/zulip-emoji-cache, ~/node, ~/misc
- &run_backend_tests # The moment of truth! Run the tests.
run:
- run:
name: run backend tests name: run backend tests
command: | command: |
. /srv/zulip-py3-venv/bin/activate . /srv/zulip-py3-venv/bin/activate
mispipe ./tools/ci/backend ts mispipe ./tools/travis/backend ts
- &run_frontend_tests - run:
run:
name: run frontend tests name: run frontend tests
command: | command: |
. /srv/zulip-py3-venv/bin/activate . /srv/zulip-py3-venv/bin/activate
mispipe ./tools/ci/frontend ts mispipe ./tools/travis/frontend ts
- &upload_coverage_report - run:
run:
name: upload coverage report name: upload coverage report
command: | command: |
. /srv/zulip-py3-venv/bin/activate . /srv/zulip-py3-venv/bin/activate
pip install codecov && codecov \ pip install codecov && codecov \
|| echo "Error in uploading coverage reports to codecov.io." || echo "Error in uploading coverage reports to codecov.io."
jobs:
"trusty-python-3.4":
docker:
# This is built from tools/circleci/images/trusty/Dockerfile .
- image: gregprice/circleci:trusty-python-5.test
working_directory: ~/zulip
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
# - store_artifacts: # TODO # - store_artifacts: # TODO
# path: var/casper/ # path: var/casper/
# # also /tmp/zulip-test-event-log/ # # also /tmp/zulip-test-event-log/
@@ -116,14 +93,50 @@ jobs:
steps: steps:
- checkout - checkout
- *create_cache_directories
- *restore_cache_package_json - run:
- *restore_cache_requirements name: create cache directories
- *install_dependencies command: |
- *save_cache_package_json dirs=(/srv/zulip-{npm,venv}-cache)
- *save_cache_requirements sudo mkdir -p "${dirs[@]}"
- *run_backend_tests sudo chown -R circleci "${dirs[@]}"
- *upload_coverage_report
- 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": "bionic-python-3.6":
docker: docker:
@@ -135,22 +148,53 @@ jobs:
steps: steps:
- checkout - checkout
- *create_cache_directories
- run: - run:
name: do Bionic hack name: create cache directories
command: | 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 # 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 # https://chat.zulip.org/#narrow/stream/3-backend/topic/Ubuntu.20bionic.20CircleCI
redis-server --daemonize yes redis-server --daemonize yes
- *restore_cache_package_json - restore_cache:
- *restore_cache_requirements keys:
- *install_dependencies - v1-npm-base.bionic-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
- *save_cache_package_json - restore_cache:
- *save_cache_requirements keys:
- *run_backend_tests - v1-venv-base.bionic-{{ checksum "requirements/thumbor.txt" }}-{{ checksum "requirements/dev.txt" }}
- *upload_coverage_report
- 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: workflows:
version: 2 version: 2

View File

@@ -26,11 +26,11 @@
"_": false, "_": false,
"activity": false, "activity": false,
"admin": false, "admin": false,
"admin_sections": false,
"alert_words": false, "alert_words": false,
"alert_words_ui": false, "alert_words_ui": false,
"attachments_ui": false, "attachments_ui": false,
"avatar": false, "avatar": false,
"billing": false,
"blueslip": false, "blueslip": false,
"bot_data": false, "bot_data": false,
"bridge": false, "bridge": false,
@@ -38,7 +38,6 @@
"buddy_list": false, "buddy_list": false,
"channel": false, "channel": false,
"click_handlers": false, "click_handlers": false,
"color_data": false,
"colorspace": false, "colorspace": false,
"common": false, "common": false,
"components": false, "components": false,
@@ -61,14 +60,12 @@
"emoji_picker": false, "emoji_picker": false,
"favicon": false, "favicon": false,
"feature_flags": false, "feature_flags": false,
"feedback_widget": false,
"fenced_code": false, "fenced_code": false,
"flatpickr": false, "flatpickr": false,
"floating_recipient_bar": false, "floating_recipient_bar": false,
"gear_menu": false, "gear_menu": false,
"hash_util": false, "hash_util": false,
"hashchange": false, "hashchange": false,
"helpers": false,
"home_msg_list": false, "home_msg_list": false,
"hotspots": false, "hotspots": false,
"i18n": false, "i18n": false,
@@ -120,7 +117,6 @@
"pygments_data": false, "pygments_data": false,
"reactions": false, "reactions": false,
"realm_icon": false, "realm_icon": false,
"realm_logo": false,
"recent_senders": false, "recent_senders": false,
"reload": false, "reload": false,
"reload_state": false, "reload_state": false,
@@ -145,7 +141,7 @@
"settings_bots": false, "settings_bots": false,
"settings_display": false, "settings_display": false,
"settings_emoji": false, "settings_emoji": false,
"settings_linkifiers": false, "settings_filters": false,
"settings_invites": false, "settings_invites": false,
"settings_muting": false, "settings_muting": false,
"settings_notifications": false, "settings_notifications": false,
@@ -168,7 +164,6 @@
"stream_muting": false, "stream_muting": false,
"stream_popover": false, "stream_popover": false,
"stream_sort": false, "stream_sort": false,
"StripeCheckout": false,
"submessage": false, "submessage": false,
"subs": false, "subs": false,
"tab_bar": false, "tab_bar": false,
@@ -190,23 +185,19 @@
"typing_events": false, "typing_events": false,
"typing_status": false, "typing_status": false,
"ui": false, "ui": false,
"ui_init": false,
"ui_report": false, "ui_report": false,
"ui_util": false, "ui_util": false,
"unread": false, "unread": false,
"unread_ops": false, "unread_ops": false,
"unread_ui": false, "unread_ui": false,
"upgrade": false,
"upload": false, "upload": false,
"upload_widget": false, "upload_widget": false,
"user_events": false, "user_events": false,
"user_groups": false, "user_groups": false,
"user_pill": false, "user_pill": false,
"user_search": false, "user_search": false,
"user_status": false,
"user_status_ui": false,
"util": false, "util": false,
"poll_widget": false, "voting_widget": false,
"widgetize": false, "widgetize": false,
"zcommand": false, "zcommand": false,
"zform": false, "zform": false,
@@ -231,12 +222,6 @@
"functions": "never" "functions": "never"
} }
], ],
"comma-spacing": [ "error",
{
"before": false,
"after": true
}
],
"complexity": [ 0, 4 ], "complexity": [ 0, 4 ],
"curly": 2, "curly": 2,
"dot-notation": [ "error", { "allowKeywords": true } ], "dot-notation": [ "error", { "allowKeywords": true } ],
@@ -254,12 +239,6 @@
"FunctionExpression": {"parameters": "first"}, "FunctionExpression": {"parameters": "first"},
"FunctionDeclaration": {"parameters": "first"} "FunctionDeclaration": {"parameters": "first"}
}], }],
"key-spacing": [ "error",
{
"beforeColon": false,
"afterColon": true
}
],
"keyword-spacing": [ "error", "keyword-spacing": [ "error",
{ {
"before": true, "before": true,
@@ -386,7 +365,6 @@
"quotes": [ 0, "single" ], "quotes": [ 0, "single" ],
"radix": 2, "radix": 2,
"semi": 2, "semi": 2,
"semi-spacing": [2, {"before": false, "after": true}],
"space-before-blocks": 2, "space-before-blocks": 2,
"space-before-function-paren": [ "error", "space-before-function-paren": [ "error",
{ {

View File

@@ -62,6 +62,5 @@
# Limit language features # Limit language features
"color-no-hex": true, "color-no-hex": true,
"color-named": "never",
} }
} }

View File

@@ -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. # high-level documentation on our Travis CI setup.
dist: trusty dist: trusty
group: deprecated-2017Q4 group: deprecated-2017Q4
@@ -15,7 +15,7 @@ install:
- mispipe "pip install codecov" ts || mispipe "pip install codecov" ts - mispipe "pip install codecov" ts || mispipe "pip install codecov" ts
# This is the main setup job for the test suite # 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 # Clean any caches that are not in use to avoid our cache
# becoming huge. # becoming huge.
@@ -26,7 +26,7 @@ script:
# broken running their system puppet with Ruby. See # broken running their system puppet with Ruby. See
# https://travis-ci.org/zulip/zulip/jobs/240120991 for an example traceback. # https://travis-ci.org/zulip/zulip/jobs/240120991 for an example traceback.
- unset GEM_PATH - unset GEM_PATH
- mispipe "./tools/ci/$TEST_SUITE" ts - mispipe "./tools/travis/$TEST_SUITE" ts
cache: cache:
yarn: true yarn: true
apt: false apt: false
@@ -38,7 +38,7 @@ cache:
- $HOME/misc - $HOME/misc
env: env:
global: global:
- BOTO_CONFIG=/nonexistent - BOTO_CONFIG=/tmp/nowhere
language: python language: python
# Our test suites generally run on Python 3.4, the version in # Our test suites generally run on Python 3.4, the version in
# Ubuntu 14.04 trusty, which is the oldest OS release we support. # Ubuntu 14.04 trusty, which is the oldest OS release we support.

View File

@@ -69,11 +69,10 @@ to help.
if you run into any troubles. if you run into any troubles.
* Read the * Read the
[Zulip guide to Git](https://zulip.readthedocs.io/en/latest/git/index.html) [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 and do the Git tutorial (coming soon) if you are unfamiliar with Git,
Git, getting help in getting help in
[#git help](https://chat.zulip.org/#narrow/stream/44-git-help) if [#git help](https://chat.zulip.org/#narrow/stream/44-git-help) if you run
you run into any troubles. Be sure to check out the into any troubles.
[extremely useful Zulip-specific tools page](https://zulip.readthedocs.io/en/latest/git/zulip-tools.html).
* Sign the * Sign the
[Dropbox Contributor License Agreement](https://opensource.dropbox.com/cla/). [Dropbox Contributor License Agreement](https://opensource.dropbox.com/cla/).

View File

@@ -11,7 +11,7 @@ RUN useradd -d /home/zulip -m zulip && echo 'zulip ALL=(ALL) NOPASSWD:ALL' >> /e
USER zulip USER zulip
RUN ln -nsf /srv/zulip ~/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/.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 WORKDIR /srv/zulip

View File

@@ -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 over 300 contributors merging over 500 commits a month, Zulip is also the
largest and fastest growing open source group chat project. largest and fastest growing open source group chat project.
[![CircleCI branch](https://img.shields.io/circleci/project/github/zulip/zulip/master.svg)](https://circleci.com/gh/zulip/zulip) [![CircleCI Build Status](https://circleci.com/gh/zulip/zulip.svg?style=svg)](https://circleci.com/gh/zulip/zulip)
[![Travis Build Status](https://travis-ci.org/zulip/zulip.svg?branch=master)](https://travis-ci.org/zulip/zulip) [![Travis Build Status](https://travis-ci.org/zulip/zulip.svg?branch=master)](https://travis-ci.org/zulip/zulip)
[![Coverage Status](https://img.shields.io/codecov/c/github/zulip/zulip.svg)](https://codecov.io/gh/zulip/zulip) [![Coverage Status](https://img.shields.io/codecov/c/github/zulip/zulip.svg)](https://codecov.io/gh/zulip/zulip)
[![Mypy coverage](https://img.shields.io/badge/mypy-100%25-green.svg)][mypy-coverage] [![Mypy coverage](https://img.shields.io/badge/mypy-100%25-green.svg)][mypy-coverage]
[![GitHub release](https://img.shields.io/github/release/zulip/zulip.svg)](https://github.com/zulip/zulip/releases/latest)
[![docs](https://readthedocs.org/projects/zulip/badge/?version=latest)](https://zulip.readthedocs.io/en/latest/) [![docs](https://readthedocs.org/projects/zulip/badge/?version=latest)](https://zulip.readthedocs.io/en/latest/)
[![Zulip chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://chat.zulip.org) [![Zulip chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://chat.zulip.org)
[![Twitter](https://img.shields.io/badge/twitter-@zulip-blue.svg?style=flat)](https://twitter.com/zulip) [![Twitter](https://img.shields.io/badge/twitter-@zulip-blue.svg?style=flat)](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 * **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, 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 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> Commercial support is available; see <https://zulipchat.com/plans>
for details. for details.

View File

@@ -2,14 +2,14 @@ import time
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from typing import Callable, Dict, List, \ from typing import Any, Callable, Dict, List, \
Optional, Tuple, Type, Union Optional, Tuple, Type, Union
from django.conf import settings from django.conf import settings
from django.db import connection from django.db import connection, models
from django.db.models import F from django.db.models import F
from analytics.models import BaseCount, \ from analytics.models import Anomaly, BaseCount, \
FillState, InstallationCount, RealmCount, StreamCount, \ FillState, InstallationCount, RealmCount, StreamCount, \
UserCount, installation_epoch, last_successful_fill UserCount, installation_epoch, last_successful_fill
from zerver.lib.logging_util import log_to_file from zerver.lib.logging_util import log_to_file
@@ -226,6 +226,7 @@ def do_drop_all_analytics_tables() -> None:
RealmCount.objects.all().delete() RealmCount.objects.all().delete()
InstallationCount.objects.all().delete() InstallationCount.objects.all().delete()
FillState.objects.all().delete() FillState.objects.all().delete()
Anomaly.objects.all().delete()
def do_drop_single_stat(property: str) -> None: def do_drop_single_stat(property: str) -> None:
UserCount.objects.filter(property=property).delete() UserCount.objects.filter(property=property).delete()

View File

@@ -1,9 +1,10 @@
from argparse import ArgumentParser
from datetime import timedelta from datetime import timedelta
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils.timezone import now as timezone_now 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 last_successful_fill
from analytics.lib.counts import COUNT_STATS, CountStat from analytics.lib.counts import COUNT_STATS, CountStat
from zerver.lib.timestamp import floor_to_hour, floor_to_day, verify_UTC, \ 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 from zerver.models import Realm
import os import os
import sys
import time import time
from typing import Any, Dict from typing import Any, Dict

View File

@@ -1,6 +1,6 @@
from datetime import datetime, timedelta 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.core.management.base import BaseCommand
from django.utils.timezone import now as timezone_now 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.lib.time_utils import time_range
from analytics.models import BaseCount, FillState, RealmCount, UserCount, \ from analytics.models import BaseCount, FillState, RealmCount, UserCount, \
StreamCount, InstallationCount 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.lib.timestamp import floor_to_day
from zerver.models import Realm, UserProfile, Stream, Client, \ from zerver.models import Realm, UserProfile, Stream, Message, Client, \
RealmAuditLog, Recipient, Subscription RealmAuditLog, Recipient
class Command(BaseCommand): class Command(BaseCommand):
help = """Populates analytics tables with randomly generated data.""" help = """Populates analytics tables with randomly generated data."""
@@ -72,16 +72,7 @@ class Command(BaseCommand):
do_change_is_admin(shylock, True) do_change_is_admin(shylock, True)
stream = Stream.objects.create( stream = Stream.objects.create(
name='all', realm=realm, date_created=installation_time) name='all', realm=realm, date_created=installation_time)
recipient = Recipient.objects.create(type_id=stream.id, type=Recipient.STREAM) 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)
def insert_fixture_data(stat: CountStat, def insert_fixture_data(stat: CountStat,
fixture_data: Mapping[Optional[str], List[int]], fixture_data: Mapping[Optional[str], List[int]],

View File

@@ -2,6 +2,7 @@ import datetime
from argparse import ArgumentParser from argparse import ArgumentParser
from typing import Any, List from typing import Any, List
import pytz
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import Count from django.db.models import Count
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now

View File

@@ -25,33 +25,20 @@ class Command(BaseCommand):
realms = Realm.objects.all() realms = Realm.objects.all()
for realm in realms: 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-")) streams = Stream.objects.filter(realm=realm).exclude(Q(name__istartswith="tutorial-"))
# private stream count invite_only_count = 0
private_count = 0
# public stream count
public_count = 0
for stream in streams: for stream in streams:
if stream.invite_only: if stream.invite_only:
private_count += 1 invite_only_count += 1
else: continue
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'
print("%25s" % (stream.name,), end=' ') print("%25s" % (stream.name,), end=' ')
recipient = Recipient.objects.filter(type=Recipient.STREAM, type_id=stream.id) recipient = Recipient.objects.filter(type=Recipient.STREAM, type_id=stream.id)
print("%10d" % (len(Subscription.objects.filter(recipient=recipient, print("%10d" % (len(Subscription.objects.filter(recipient=recipient,
active=True)),), end=' ') active=True)),), end=' ')
num_messages = len(Message.objects.filter(recipient=recipient)) num_messages = len(Message.objects.filter(recipient=recipient))
print("%12d" % (num_messages,), end=' ') print("%12d" % (num_messages,))
print("%15s" % (stream_type,)) print("%d private streams" % (invite_only_count,))
print("") print("")

View File

@@ -3,6 +3,8 @@ import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import zerver.lib.str_utils
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import migrations from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import migrations, models from django.db import migrations, models
import zerver.lib.str_utils
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import migrations from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-16 20:50 # Generated by Django 1.10.4 on 2017-01-16 20:50
from django.conf import settings
from django.db import migrations from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -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',
),
]

View File

@@ -1,10 +1,10 @@
import datetime import datetime
from typing import Optional from typing import Any, Dict, Optional, Tuple, Union
from django.db import models from django.db import models
from zerver.lib.timestamp import floor_to_day 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): class FillState(models.Model):
property = models.CharField(max_length=40, unique=True) # type: str 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
return fillstate.end_time - datetime.timedelta(hours=1) 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): class BaseCount(models.Model):
# Note: When inheriting from BaseCount, you may want to rearrange # Note: When inheriting from BaseCount, you may want to rearrange
# the order of the columns in the migration to make sure they # 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] subgroup = models.CharField(max_length=16, null=True) # type: Optional[str]
end_time = models.DateTimeField() # type: datetime.datetime end_time = models.DateTimeField() # type: datetime.datetime
value = models.BigIntegerField() # type: int value = models.BigIntegerField() # type: int
anomaly = models.ForeignKey(Anomaly, on_delete=models.SET_NULL, null=True) # type: Optional[Anomaly]
class Meta: class Meta:
abstract = True abstract = True

View File

@@ -1,6 +1,6 @@
from datetime import datetime, timedelta 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 import ujson
from django.apps import apps 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 now as timezone_now
from django.utils.timezone import utc as timezone_utc 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, \ DependentCountStat, LoggingCountStat, do_aggregate_to_summary_table, \
do_drop_all_analytics_tables, do_drop_single_stat, \ do_drop_all_analytics_tables, do_drop_single_stat, \
do_fill_count_stat_at_hour, do_increment_logging_stat, \ do_fill_count_stat_at_hour, do_increment_logging_stat, \
process_count_stat, sql_data_collector process_count_stat, sql_data_collector
from analytics.models import BaseCount, \ from analytics.models import Anomaly, BaseCount, \
FillState, InstallationCount, RealmCount, StreamCount, \ FillState, InstallationCount, RealmCount, StreamCount, \
UserCount, installation_epoch UserCount, installation_epoch, last_successful_fill
from zerver.lib.actions import do_activate_user, do_create_user, \ from zerver.lib.actions import do_activate_user, do_create_user, \
do_deactivate_user, do_reactivate_user, update_user_activity_interval, \ do_deactivate_user, do_reactivate_user, update_user_activity_interval, \
do_invite_users, do_revoke_user_invite, do_resend_user_invite_email, \ do_invite_users, do_revoke_user_invite, do_resend_user_invite_email, \
InvitationError InvitationError
from zerver.lib.timestamp import TimezoneNotUTCException, floor_to_day 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, \ from zerver.models import Client, Huddle, Message, Realm, \
RealmAuditLog, Recipient, Stream, UserActivityInterval, \ RealmAuditLog, Recipient, Stream, UserActivityInterval, \
UserProfile, get_client, get_user, PreregistrationUser UserProfile, get_client, get_user, PreregistrationUser
@@ -57,10 +56,7 @@ class AnalyticsTestCase(TestCase):
'api_key': '42'} 'api_key': '42'}
for key, value in defaults.items(): for key, value in defaults.items():
kwargs[key] = kwargs.get(key, value) kwargs[key] = kwargs.get(key, value)
kwargs['delivery_email'] = kwargs['email'] return UserProfile.objects.create(**kwargs)
user_profile = UserProfile.objects.create(**kwargs)
# TODO: Make this pass user_profile.full_clean()
return user_profile
def create_stream_with_recipient(self, **kwargs: Any) -> Tuple[Stream, Recipient]: def create_stream_with_recipient(self, **kwargs: Any) -> Tuple[Stream, Recipient]:
self.name_counter += 1 self.name_counter += 1
@@ -86,7 +82,7 @@ class AnalyticsTestCase(TestCase):
defaults = { defaults = {
'sender': sender, 'sender': sender,
'recipient': recipient, 'recipient': recipient,
DB_TOPIC_NAME: 'subject', 'subject': 'subject',
'content': 'hi', 'content': 'hi',
'pub_date': self.TIME_LAST_HOUR, 'pub_date': self.TIME_LAST_HOUR,
'sending_client': get_client("website")} 'sending_client': get_client("website")}
@@ -846,6 +842,7 @@ class TestDeleteStats(AnalyticsTestCase):
RealmCount.objects.create(realm=user.realm, **count_args) RealmCount.objects.create(realm=user.realm, **count_args)
InstallationCount.objects.create(**count_args) InstallationCount.objects.create(**count_args)
FillState.objects.create(property='test', end_time=self.TIME_ZERO, state=FillState.DONE) 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') analytics = apps.get_app_config('analytics')
for table in list(analytics.models.values()): for table in list(analytics.models.values()):
@@ -868,6 +865,7 @@ class TestDeleteStats(AnalyticsTestCase):
InstallationCount.objects.create(**count_args) InstallationCount.objects.create(**count_args)
FillState.objects.create(property='to_delete', end_time=self.TIME_ZERO, state=FillState.DONE) 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) 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') analytics = apps.get_app_config('analytics')
for table in list(analytics.models.values()): for table in list(analytics.models.values()):
@@ -875,6 +873,9 @@ class TestDeleteStats(AnalyticsTestCase):
do_drop_single_stat('to_delete') do_drop_single_stat('to_delete')
for table in list(analytics.models.values()): for table in list(analytics.models.values()):
if table._meta.db_table == 'analytics_anomaly':
self.assertTrue(table.objects.exists())
else:
self.assertFalse(table.objects.filter(property='to_delete').exists()) self.assertFalse(table.objects.filter(property='to_delete').exists())
self.assertTrue(table.objects.filter(property='to_save').exists()) self.assertTrue(table.objects.filter(property='to_save').exists())

View File

@@ -1,5 +1,5 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Optional from typing import Dict, List, Optional
import mock import mock
from django.utils.timezone import utc 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.lib.time_utils import time_range
from analytics.models import FillState, \ from analytics.models import FillState, \
RealmCount, UserCount, last_successful_fill RealmCount, UserCount, last_successful_fill
from analytics.views import rewrite_client_arrays, \ from analytics.views import get_chart_data, rewrite_client_arrays, \
sort_by_totals, sort_client_labels sort_by_totals, sort_client_labels, stats
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.timestamp import ceiling_to_day, \ from zerver.lib.timestamp import ceiling_to_day, \
ceiling_to_hour, datetime_to_timestamp ceiling_to_hour, datetime_to_timestamp

View File

@@ -16,12 +16,6 @@ i18n_urlpatterns = [
name='analytics.views.stats_for_realm'), name='analytics.views.stats_for_realm'),
url(r'^stats/installation$', analytics.views.stats_for_installation, url(r'^stats/installation$', analytics.views.stats_for_installation,
name='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 # User-visible stats page
url(r'^stats$', analytics.views.stats, url(r'^stats$', analytics.views.stats,
@@ -44,11 +38,6 @@ v1_api_and_json_patterns = [
{'GET': 'analytics.views.get_chart_data_for_realm'}), {'GET': 'analytics.views.get_chart_data_for_realm'}),
url(r'^analytics/chart_data/installation$', rest_dispatch, url(r'^analytics/chart_data/installation$', rest_dispatch,
{'GET': 'analytics.views.get_chart_data_for_installation'}), {'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 += [ i18n_urlpatterns += [

View File

@@ -1,26 +1,28 @@
import itertools import itertools
import json
import logging import logging
import re import re
import time import time
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Callable, Dict, List, \ from typing import Any, Callable, Dict, List, \
Optional, Set, Tuple, Type, Union, cast Optional, Set, Tuple, Type, Union
import pytz import pytz
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from django.db import connection from django.db import connection
from django.db.models import Sum
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.http import HttpRequest, HttpResponse, HttpResponseNotFound from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
from django.shortcuts import render 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.timezone import now as timezone_now, utc as timezone_utc
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from jinja2 import Markup as mark_safe 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.lib.time_utils import time_range
from analytics.models import BaseCount, InstallationCount, \ from analytics.models import BaseCount, InstallationCount, \
RealmCount, StreamCount, UserCount, last_successful_fill, installation_epoch 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.json_encoder_for_html import JSONEncoderForHTML
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_success 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, \ from zerver.models import Client, get_realm, Realm, \
UserActivity, UserActivityInterval, UserProfile 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, 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( page_params = dict(
data_url_suffix=data_url_suffix, data_url_suffix=data_url_suffix,
for_installation=for_installation, for_installation=for_installation,
remote=remote,
debug_mode=False, debug_mode=False,
) )
return render(request, 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) 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 @require_server_admin_api
@has_request_variables @has_request_variables
def get_chart_data_for_realm(request: HttpRequest, user_profile: UserProfile, 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) 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 @require_server_admin
def stats_for_installation(request: HttpRequest) -> HttpResponse: def stats_for_installation(request: HttpRequest) -> HttpResponse:
return render_stats(request, '/installation', 'Installation', True) 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 @require_server_admin_api
@has_request_variables @has_request_variables
def get_chart_data_for_installation(request: HttpRequest, user_profile: UserProfile, def get_chart_data_for_installation(request: HttpRequest, user_profile: UserProfile,
chart_name: str=REQ(), **kwargs: Any) -> HttpResponse: chart_name: str=REQ(), **kwargs: Any) -> HttpResponse:
return get_chart_data(request=request, user_profile=user_profile, for_installation=True, **kwargs) 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 @require_non_guest_user
@has_request_variables @has_request_variables
def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name: str=REQ(), 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), min_length: Optional[int]=REQ(converter=to_non_negative_int, default=None),
start: Optional[datetime]=REQ(converter=to_utc_datetime, default=None), start: Optional[datetime]=REQ(converter=to_utc_datetime, default=None),
end: 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, realm: Optional[Realm]=None, for_installation: bool=False) -> HttpResponse:
remote: bool=False, remote_realm_id: Optional[int]=None,
server: Optional[RemoteZulipServer]=None) -> HttpResponse:
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 = RealmCount
if for_installation:
aggregate_table = InstallationCount
if chart_name == 'number_of_humans': if chart_name == 'number_of_humans':
stats = [ stats = [
@@ -201,24 +147,7 @@ def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name:
{'start': start, 'end': end}) {'start': start, 'end': end})
if realm is None: 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 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 start is None:
if for_installation: if for_installation:
start = installation_epoch() start = installation_epoch()
@@ -238,23 +167,9 @@ def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name:
end_times = time_range(start, end, stats[0].frequency, min_length) end_times = time_range(start, end, stats[0].frequency, min_length)
data = {'end_times': end_times, 'frequency': stats[0].frequency} # type: Dict[str, Any] data = {'end_times': end_times, 'frequency': stats[0].frequency} # type: Dict[str, Any]
aggregation_level = { aggregation_level = {InstallationCount: 'everyone', RealmCount: 'everyone', UserCount: 'user'}
InstallationCount: 'everyone',
RealmCount: 'everyone',
RemoteInstallationCount: 'everyone',
RemoteRealmCount: 'everyone',
UserCount: 'user',
}
# -1 is a placeholder value, since there is no relevant filtering on InstallationCount # -1 is a placeholder value, since there is no relevant filtering on InstallationCount
id_value = { id_value = {InstallationCount: -1, RealmCount: realm.id, UserCount: user_profile.id}
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,
}
for table in tables: for table in tables:
data[aggregation_level[table]] = {} data[aggregation_level[table]] = {}
for stat in stats: 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) return StreamCount.objects.filter(stream_id=key_id)
elif table == InstallationCount: elif table == InstallationCount:
return InstallationCount.objects.all() 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: else:
raise AssertionError("Unknown table: %s" % (table,)) raise AssertionError("Unknown table: %s" % (table,))
@@ -565,7 +476,7 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
for row in rows: for row in rows:
row['date_created_day'] = row['date_created'].strftime('%Y-%m-%d') row['date_created_day'] = row['date_created'].strftime('%Y-%m-%d')
row['plan_type_string'] = [ 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() row['age_days'] = int((now - row['date_created']).total_seconds()
/ 86400) / 86400)
row['is_new'] = row['age_days'] < 12 * 7 row['is_new'] = row['age_days'] < 12 * 7
@@ -579,16 +490,6 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
except Exception: except Exception:
row['history'] = '' 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 # augment data with realm_minutes
total_hours = 0.0 total_hours = 0.0
for row in rows: 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_bot_count += int(row['bot_count'])
total_wau_count += int(row['wau_count']) total_wau_count += int(row['wau_count'])
total_row = dict( rows.append(dict(
string_id='Total', string_id='Total',
plan_type_string="", plan_type_string="",
amount=total_amount,
stats_link = '', stats_link = '',
date_created_day='', date_created_day='',
realm_admin_email='', realm_admin_email='',
@@ -636,9 +536,7 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
bot_count=total_bot_count, bot_count=total_bot_count,
hours=int(total_hours), hours=int(total_hours),
wau_count=total_wau_count, wau_count=total_wau_count,
) ))
rows.insert(0, total_row)
content = loader.render_to_string( content = loader.render_to_string(
'analytics/realm_summary_table.html', 'analytics/realm_summary_table.html',
@@ -664,15 +562,15 @@ def user_activity_intervals() -> Tuple[mark_safe, Dict[str, float]]:
).only( ).only(
'start', 'start',
'end', 'end',
'user_profile__delivery_email', 'user_profile__email',
'user_profile__realm__string_id' 'user_profile__realm__string_id'
).order_by( ).order_by(
'user_profile__realm__string_id', 'user_profile__realm__string_id',
'user_profile__delivery_email' 'user_profile__email'
) )
by_string_id = lambda row: row.user_profile.realm.string_id 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 = {} realm_minutes = {}
@@ -766,8 +664,7 @@ def sent_messages_report(realm: str) -> str:
return make_table(title, cols, rows) return make_table(title, cols, rows)
def ad_hoc_queries() -> List[Dict[str, str]]: def ad_hoc_queries() -> List[Dict[str, str]]:
def get_page(query: str, cols: List[str], title: str, def get_page(query: str, cols: List[str], title: str) -> Dict[str, str]:
totals_columns: List[int]=[]) -> Dict[str, str]:
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute(query) cursor.execute(query)
rows = cursor.fetchall() rows = cursor.fetchall()
@@ -779,24 +676,11 @@ def ad_hoc_queries() -> List[Dict[str, str]]:
for row in rows: for row in rows:
row[i] = fixup_func(row[i]) row[i] = fixup_func(row[i])
total_row = []
for i, col in enumerate(cols): for i, col in enumerate(cols):
if col == 'Realm': if col == 'Realm':
fix_rows(i, realm_activity_link) fix_rows(i, realm_activity_link)
elif col in ['Last time', 'Last visit']: elif col in ['Last time', 'Last visit']:
fix_rows(i, format_date_for_activity_reports) 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) 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)) 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 return pages
@require_server_admin @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: def get_user_activity_records_for_realm(realm: str, is_bot: bool) -> QuerySet:
fields = [ fields = [
'user_profile__full_name', 'user_profile__full_name',
'user_profile__delivery_email', 'user_profile__email',
'query', 'query',
'client__name', 'client__name',
'count', '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_active=True,
user_profile__is_bot=is_bot 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) records = records.select_related('user_profile', 'client').only(*fields)
return records return records
@@ -1040,7 +881,7 @@ def get_user_activity_records_for_email(email: str) -> List[QuerySet]:
] ]
records = UserActivity.objects.filter( records = UserActivity.objects.filter(
user_profile__delivery_email=email user_profile__email=email
) )
records = records.order_by("-last_visit") records = records.order_by("-last_visit")
records = records.select_related('user_profile', 'client').only(*fields) 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) stats_link = '<a href="{}"><i class="fa fa-pie-chart"></i></a>'.format(url, realm_str)
return mark_safe(stats_link) 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: def realm_client_table(user_summaries: Dict[str, Dict[str, Dict[str, Any]]]) -> str:
exclude_keys = [ exclude_keys = [
'internal', 'internal',
@@ -1220,7 +1055,7 @@ def realm_user_summary_table(all_records: List[QuerySet],
user_records = {} user_records = {}
def by_email(record: QuerySet) -> str: 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): for email, records in itertools.groupby(all_records, by_email):
user_records[email] = get_user_activity_summary(list(records)) 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: except Realm.DoesNotExist:
return HttpResponseNotFound("Realm %s does not exist" % (realm_str,)) 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')]: for is_bot, page_title in [(False, 'Humans'), (True, 'Bots')]:
all_records = list(get_user_activity_records_for_realm(realm_str, is_bot)) all_records = list(get_user_activity_records_for_realm(realm_str, is_bot))

View File

@@ -16,11 +16,12 @@ from django.http import HttpRequest, HttpResponse
from django.shortcuts import render from django.shortcuts import render
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from zerver.lib.utils import generate_random_token
from zerver.models import PreregistrationUser, EmailChangeStatus, MultiuseInvite, \ from zerver.models import PreregistrationUser, EmailChangeStatus, MultiuseInvite, \
UserProfile, Realm UserProfile, Realm
from random import SystemRandom from random import SystemRandom
import string import string
from typing import Dict, Optional, Union from typing import Any, Dict, Optional, Union
class ConfirmationKeyException(Exception): class ConfirmationKeyException(Exception):
WRONG_LENGTH = 1 WRONG_LENGTH = 1
@@ -69,11 +70,8 @@ def create_confirmation_link(obj: ContentType, host: str,
confirmation_type: int, confirmation_type: int,
url_args: Optional[Dict[str, str]]=None) -> str: url_args: Optional[Dict[str, str]]=None) -> str:
key = generate_key() 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, 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) return confirmation_url(key, host, confirmation_type, url_args)
def confirmation_url(confirmation_key: str, host: str, def confirmation_url(confirmation_key: str, host: str,
@@ -101,7 +99,6 @@ class Confirmation(models.Model):
SERVER_REGISTRATION = 5 SERVER_REGISTRATION = 5
MULTIUSE_INVITE = 6 MULTIUSE_INVITE = 6
REALM_CREATION = 7 REALM_CREATION = 7
REALM_REACTIVATION = 8
type = models.PositiveSmallIntegerField() # type: int type = models.PositiveSmallIntegerField() # type: int
def __str__(self) -> str: def __str__(self) -> str:
@@ -124,18 +121,8 @@ _properties = {
'zerver.views.registration.accounts_home_from_multiuse_invite', 'zerver.views.registration.accounts_home_from_multiuse_invite',
validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS), validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS),
Confirmation.REALM_CREATION: ConfirmationType('check_prereg_key_and_redirect'), 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 # Functions related to links generated by the generate_realm_creation_link.py
# management command. # management command.
# Note that being validated here will just allow the user to access the create_realm # Note that being validated here will just allow the user to access the create_realm

View File

@@ -2,6 +2,8 @@
# Copyright: (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com> # 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 $' __revision__ = '$Id: settings.py 12 2008-11-23 19:38:52Z jarek.zgoda $'
STATUS_ACTIVE = 1 STATUS_ACTIVE = 1

View File

@@ -1,10 +1,8 @@
from datetime import datetime import datetime
from decimal import Decimal
from functools import wraps from functools import wraps
import logging import logging
import math
import os import os
from typing import Any, Callable, Dict, Optional, TypeVar, Tuple, cast from typing import Any, Callable, Dict, Optional, TypeVar, Tuple
import ujson import ujson
from django.conf import settings from django.conf import settings
@@ -14,12 +12,13 @@ from django.utils.timezone import now as timezone_now
from django.core.signing import Signer from django.core.signing import Signer
import stripe import stripe
from zerver.lib.exceptions import JsonableError
from zerver.lib.logging_util import log_to_file from zerver.lib.logging_util import log_to_file
from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime
from zerver.lib.utils import generate_random_token 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 zerver.models import Realm, UserProfile, RealmAuditLog
from corporate.models import Customer, CustomerPlan, LicenseLedger, \ from corporate.models import Customer, Plan, Coupon, BillingProcessor
get_active_plan
from zproject.settings import get_secret from zproject.settings import get_secret
STRIPE_PUBLISHABLE_KEY = get_secret('stripe_publishable_key') 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(billing_logger, BILLING_LOG_PATH)
log_to_file(logging.getLogger('stripe'), 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]) CallableT = TypeVar('CallableT', bound=Callable[..., Any])
MIN_INVOICED_LICENSES = 30
DEFAULT_INVOICE_DAYS_UNTIL_DUE = 30
def get_seat_count(realm: Realm) -> int: def get_seat_count(realm: Realm) -> int:
non_guests = UserProfile.objects.filter( return UserProfile.objects.filter(realm=realm, is_active=True, is_bot=False).count()
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))
def sign_string(string: str) -> Tuple[str, str]: def sign_string(string: str) -> Tuple[str, str]:
salt = generate_random_token(64) salt = generate_random_token(64)
@@ -54,78 +66,13 @@ def unsign_string(signed_string: str, salt: str) -> str:
signer = Signer(salt=salt) signer = Signer(salt=salt)
return signer.unsign(signed_string) 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): class BillingError(Exception):
# error messages # error messages
CONTACT_SUPPORT = _("Something went wrong. Please contact %s." % (settings.ZULIP_ADMINISTRATOR,)) CONTACT_SUPPORT = _("Something went wrong. Please contact %s." % (settings.ZULIP_ADMINISTRATOR,))
TRY_RELOADING = _("Something went wrong. Please reload the page.") TRY_RELOADING = _("Something went wrong. Please reload the page.")
# description is used only for tests # 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.description = description
self.message = message self.message = message
@@ -142,6 +89,9 @@ def catch_stripe_errors(func: CallableT) -> CallableT:
if STRIPE_PUBLISHABLE_KEY is None: if STRIPE_PUBLISHABLE_KEY is None:
raise BillingError('missing stripe config', "Missing Stripe config. " raise BillingError('missing stripe config', "Missing Stripe config. "
"See https://zulip.readthedocs.io/en/latest/subsystems/billing.html.") "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: try:
return func(*args, **kwargs) return func(*args, **kwargs)
# See https://stripe.com/docs/api/python#error_handling, though # See https://stripe.com/docs/api/python#error_handling, though
@@ -164,11 +114,58 @@ def catch_stripe_errors(func: CallableT) -> CallableT:
@catch_stripe_errors @catch_stripe_errors
def stripe_get_customer(stripe_customer_id: str) -> stripe.Customer: 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 @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 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 # 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 # 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 # 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), description="%s (%s)" % (realm.string_id, realm.name),
email=user.email, email=user.email,
metadata={'realm_id': realm.id, 'realm_str': realm.string_id}, 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) event_time = timestamp_to_datetime(stripe_customer.created)
with transaction.atomic(): with transaction.atomic():
RealmAuditLog.objects.create( RealmAuditLog.objects.create(
@@ -187,11 +187,10 @@ def do_create_stripe_customer(user: UserProfile, stripe_token: Optional[str]=Non
RealmAuditLog.objects.create( RealmAuditLog.objects.create(
realm=user.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_CARD_CHANGED, realm=user.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_CARD_CHANGED,
event_time=event_time) event_time=event_time)
customer, created = Customer.objects.update_or_create(realm=realm, defaults={ Customer.objects.create(realm=realm, stripe_customer_id=stripe_customer.id)
'stripe_customer_id': stripe_customer.id})
user.is_billing_admin = True user.is_billing_admin = True
user.save(update_fields=["is_billing_admin"]) user.save(update_fields=["is_billing_admin"])
return customer return stripe_customer
@catch_stripe_errors @catch_stripe_errors
def do_replace_payment_source(user: UserProfile, stripe_token: str) -> stripe.Customer: 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()) event_time=timezone_now())
return updated_stripe_customer 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 @catch_stripe_errors
def process_initial_upgrade(user: UserProfile, licenses: int, automanage_licenses: bool, def do_replace_coupon(user: UserProfile, coupon: Coupon) -> stripe.Customer:
billing_schedule: int, stripe_token: Optional[str]) -> None: stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id)
realm = user.realm stripe_customer.coupon = coupon.stripe_coupon_id
customer = update_or_create_stripe_customer(user, stripe_token=stripe_token) return stripe.Customer.save(stripe_customer)
if CustomerPlan.objects.filter(customer=customer, status=CustomerPlan.ACTIVE).exists():
# Unlikely race condition from two people upgrading (clicking "Make payment") @catch_stripe_errors
# at exactly the same time. Doesn't fully resolve the race condition, but having def do_subscribe_customer_to_plan(user: UserProfile, stripe_customer: stripe.Customer, stripe_plan_id: str,
# a check here reduces the likelihood. seat_count: int, tax_percent: float) -> None:
billing_logger.warning( if extract_current_subscription(stripe_customer) is not None:
"Customer {} trying to upgrade, but has an active subscription".format(customer)) # 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) 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( # Success here implies the stripe_customer was charged: https://stripe.com/docs/billing/lifecycle#active
automanage_licenses, billing_schedule, customer.default_discount) # Otherwise we should expect it to throw a stripe.error.
# The main design constraint in this function is that if you upgrade with a credit card, and the stripe_subscription = stripe.Subscription.create(
# charge fails, everything should be rolled back as if nothing had happened. This is because we customer=stripe_customer.id,
# expect frequent card failures on initial signup. billing='charge_automatically',
# Hence, if we're going to charge a card, do it at the beginning, even if we later may have to items=[{
# adjust the number of licenses. 'plan': stripe_plan_id,
charge_automatically = stripe_token is not None 'quantity': seat_count,
if charge_automatically: }],
stripe_charge = stripe.Charge.create( prorate=True,
amount=price_per_license * licenses, tax_percent=tax_percent)
currency='usd', if PRINT_STRIPE_FIXTURE_DATA:
customer=customer.stripe_customer_id, print(''.join(['"create_subscription": ', str(stripe_subscription), ','])) # nocoverage
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
with transaction.atomic(): with transaction.atomic():
# billed_licenses can greater than licenses if users are added between the start of customer.has_billing_relationship = True
# this function (process_initial_upgrade) and now customer.save(update_fields=['has_billing_relationship'])
billed_licenses = max(get_seat_count(realm), licenses) customer.realm.has_seat_based_plan = True
plan_params = { customer.realm.save(update_fields=['has_seat_based_plan'])
'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'])
RealmAuditLog.objects.create( RealmAuditLog.objects.create(
realm=realm, acting_user=user, event_time=billing_cycle_anchor, realm=customer.realm,
event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED, acting_user=user,
extra_data=ujson.dumps(plan_params)) event_type=RealmAuditLog.STRIPE_PLAN_CHANGED,
stripe.InvoiceItem.create( event_time=timestamp_to_datetime(stripe_subscription.created),
currency='usd', extra_data=ujson.dumps({'plan': stripe_plan_id, 'quantity': seat_count}))
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)
if charge_automatically: current_seat_count = get_seat_count(customer.realm)
billing_method = 'charge_automatically' if seat_count != current_seat_count:
days_until_due = None RealmAuditLog.objects.create(
else: realm=customer.realm,
billing_method = 'send_invoice' event_type=RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET,
days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE event_time=timestamp_to_datetime(stripe_subscription.created),
stripe_invoice = stripe.Invoice.create( requires_billing_update=True,
auto_advance=True, extra_data=ujson.dumps({'quantity': current_seat_count}))
billing=billing_method,
customer=customer.stripe_customer_id,
days_until_due=days_until_due,
statement_descriptor='Zulip Standard')
stripe.Invoice.finalize_invoice(stripe_invoice)
from zerver.lib.actions import do_change_plan_type def process_initial_upgrade(user: UserProfile, plan: Plan, seat_count: int, stripe_token: str) -> None:
do_change_plan_type(realm, Realm.STANDARD) customer = Customer.objects.filter(realm=user.realm).first()
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()
if customer is None: if customer is None:
return stripe_customer = do_create_customer(user, stripe_token=stripe_token)
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)
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}
else: else:
assert(plan.price_per_license is not None) # needed for mypy stripe_customer = do_replace_payment_source(user, stripe_token)
price_args = {'unit_amount': plan.price_per_license, do_subscribe_customer_to_plan(
'quantity': ledger_entry.licenses} user=user,
description = "Zulip Standard - renewal" stripe_customer=stripe_customer,
elif ledger_entry.licenses != licenses_base: stripe_plan_id=plan.stripe_plan_id,
assert(plan.price_per_license) seat_count=seat_count,
last_renewal = LicenseLedger.objects.filter( # TODO: billing address details are passed to us in the request;
plan=plan, is_renewal=True, event_time__lte=ledger_entry.event_time) \ # use that to calculate taxes.
.order_by('-id').first().event_time tax_percent=0)
period_end = next_renewal_date(plan, ledger_entry.event_time) do_change_plan_type(user, Realm.STANDARD)
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: def attach_discount_to_realm(user: UserProfile, percent_off: int) -> None:
plan.invoiced_through = ledger_entry coupon = Coupon.objects.get(percent_off=percent_off)
plan.invoicing_status = CustomerPlan.STARTED customer = Customer.objects.filter(realm=user.realm).first()
plan.save(update_fields=['invoicing_status', 'invoiced_through']) if customer is None:
idempotency_key = 'ledger_entry:{}'.format(ledger_entry.id) # type: Optional[str] do_create_customer(user, coupon=coupon)
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: else:
billing_method = 'send_invoice' do_replace_coupon(user, coupon)
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) @catch_stripe_errors
plan.save(update_fields=['next_invoice_date']) 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)
def invoice_plans_as_needed(event_time: datetime) -> None: ## Process RealmAuditLog
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: def do_set_subscription_quantity(
Customer.objects.update_or_create(realm=realm, defaults={'default_discount': discount}) 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 process_downgrade(user: UserProfile) -> None: # nocoverage def do_adjust_subscription_quantity(
pass 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 estimate_annual_recurring_revenue_by_realm() -> Dict[str, int]: # nocoverage def increment_subscription_quantity(
annual_revenue = {} customer: Customer, timestamp: int, idempotency_key: str) -> None:
for plan in CustomerPlan.objects.filter( return do_adjust_subscription_quantity(customer, timestamp, idempotency_key, 1)
status=CustomerPlan.ACTIVE).select_related('customer__realm'):
# TODO: figure out what to do for plans that don't automatically def decrement_subscription_quantity(
# renew, but which probably will renew customer: Customer, timestamp: int, idempotency_key: str) -> None:
renewal_cents = renewal_amount(plan, timezone_now()) or 0 return do_adjust_subscription_quantity(customer, timestamp, idempotency_key, -1)
if plan.billing_schedule == CustomerPlan.MONTHLY:
renewal_cents *= 12 @catch_stripe_errors
# TODO: Decimal stuff def process_billing_log_entry(processor: BillingProcessor, log_row: RealmAuditLog) -> None:
annual_revenue[plan.customer.realm.string_id] = int(renewal_cents / 100) processor.state = BillingProcessor.STARTED
return annual_revenue 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:
processor.state = BillingProcessor.STALLED
processor.save()
return True
raise

View 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)

View 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)

View File

@@ -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),
),
]

View File

@@ -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')),
],
),
]

View File

@@ -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')),
],
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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',
),
]

View File

@@ -1,70 +1,46 @@
import datetime import datetime
from decimal import Decimal
from typing import Optional
from django.db import models 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): class Customer(models.Model):
realm = models.OneToOneField(Realm, on_delete=CASCADE) # type: Realm realm = models.OneToOneField(Realm, on_delete=models.CASCADE) # type: Realm
stripe_customer_id = models.CharField(max_length=255, null=True, unique=True) # type: str stripe_customer_id = models.CharField(max_length=255, unique=True) # type: str
# A percentage, like 85. # Becomes True the first time a payment successfully goes through, and never
default_discount = models.DecimalField(decimal_places=4, max_digits=7, null=True) # type: Optional[Decimal] # goes back to being False
has_billing_relationship = models.BooleanField(default=False) # type: bool
def __str__(self) -> str: def __str__(self) -> str:
return "<Customer %s %s>" % (self.realm, self.stripe_customer_id) return "<Customer %s %s>" % (self.realm, self.stripe_customer_id)
class CustomerPlan(models.Model): class Plan(models.Model):
customer = models.ForeignKey(Customer, on_delete=CASCADE) # type: Customer # The two possible values for nickname
automanage_licenses = models.BooleanField(default=False) # type: bool CLOUD_MONTHLY = 'monthly'
charge_automatically = models.BooleanField(default=False) # type: bool 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 stripe_plan_id = models.CharField(max_length=255, unique=True) # type: str
# 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]
# Discount that was applied. For display purposes only. class Coupon(models.Model):
discount = models.DecimalField(decimal_places=4, max_digits=6, null=True) # type: Optional[Decimal] 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 def __str__(self) -> str:
ANNUAL = 1 return '<Coupon: %s %s %s>' % (self.percent_off, self.stripe_coupon_id, self.id)
MONTHLY = 2
billing_schedule = models.SmallIntegerField() # type: int
next_invoice_date = models.DateTimeField(db_index=True) # type: datetime.datetime class BillingProcessor(models.Model):
invoiced_through = models.ForeignKey( log_row = models.ForeignKey(RealmAuditLog, on_delete=models.CASCADE) # RealmAuditLog
'LicenseLedger', null=True, on_delete=CASCADE, related_name='+') # type: Optional[LicenseLedger] # Exactly one processor, the global processor, has realm=None.
DONE = 1 realm = models.OneToOneField(Realm, null=True, on_delete=models.CASCADE) # type: Realm
STARTED = 2
invoicing_status = models.SmallIntegerField(default=DONE) # type: int
STANDARD = 1 DONE = 'done'
PLUS = 2 # not available through self-serve signup STARTED = 'started'
ENTERPRISE = 10 SKIPPED = 'skipped' # global processor only
tier = models.SmallIntegerField() # type: int STALLED = 'stalled' # realm processors only
state = models.CharField(max_length=20) # type: str
ACTIVE = 1 last_modified = models.DateTimeField(auto_now=True) # type: datetime.datetime
ENDED = 2
NEVER_STARTED = 3
# You can only have 1 active subscription at a time
status = models.SmallIntegerField(default=ACTIVE) # type: int
# TODO maybe override setattr to ensure billing_cycle_anchor, etc are immutable def __str__(self) -> str:
return '<BillingProcessor: %s %s %s>' % (self.realm, self.log_row, self.id)
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]

View 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
},
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,18 +1,14 @@
{ {
"account_balance": 0, "account_balance": 0,
"created": 1548695523, "created": 1539881153,
"currency": null, "currency": null,
"default_source": "card_1DxdeRGh0CmXqmnwu3BibWtM", "default_source": "card_1DMedAGh0CmXqmnwDLwrAV1v",
"delinquent": false, "delinquent": false,
"description": "zulip (Zulip Dev)", "description": "zulip (Zulip Dev)",
"discount": null, "discount": null,
"email": "hamlet@zulip.com", "email": "hamlet@zulip.com",
"id": "cus_EQUd48LphR5ahk", "id": "cus_DoHBcS2dBGOP9t",
"invoice_prefix": "9F53235", "invoice_prefix": "3B3F5D6",
"invoice_settings": {
"custom_fields": null,
"footer": null
},
"livemode": false, "livemode": false,
"metadata": { "metadata": {
"realm_id": "1", "realm_id": "1",
@@ -33,14 +29,14 @@
"address_zip_check": "pass", "address_zip_check": "pass",
"brand": "Visa", "brand": "Visa",
"country": "US", "country": "US",
"customer": "cus_EQUd48LphR5ahk", "customer": "cus_DoHBcS2dBGOP9t",
"cvc_check": "pass", "cvc_check": "pass",
"dynamic_last4": null, "dynamic_last4": null,
"exp_month": 3, "exp_month": 3,
"exp_year": 2033, "exp_year": 2033,
"fingerprint": "6dAXT9VZvwro65EK", "fingerprint": "6dAXT9VZvwro65EK",
"funding": "credit", "funding": "credit",
"id": "card_1DxdeRGh0CmXqmnwu3BibWtM", "id": "card_1DMedAGh0CmXqmnwDLwrAV1v",
"last4": "4242", "last4": "4242",
"metadata": {}, "metadata": {},
"name": "Ada Starr", "name": "Ada Starr",
@@ -51,14 +47,14 @@
"has_more": false, "has_more": false,
"object": "list", "object": "list",
"total_count": 1, "total_count": 1,
"url": "/v1/customers/cus_EQUd48LphR5ahk/sources" "url": "/v1/customers/cus_DoHBcS2dBGOP9t/sources"
}, },
"subscriptions": { "subscriptions": {
"data": [], "data": [],
"has_more": false, "has_more": false,
"object": "list", "object": "list",
"total_count": 0, "total_count": 0,
"url": "/v1/customers/cus_EQUd48LphR5ahk/subscriptions" "url": "/v1/customers/cus_DoHBcS2dBGOP9t/subscriptions"
}, },
"tax_info": null, "tax_info": null,
"tax_info_verification": null "tax_info_verification": null

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -16,7 +16,7 @@
"exp_year": 2033, "exp_year": 2033,
"fingerprint": "6dAXT9VZvwro65EK", "fingerprint": "6dAXT9VZvwro65EK",
"funding": "credit", "funding": "credit",
"id": "card_1DxdeRGh0CmXqmnwu3BibWtM", "id": "card_1DMedAGh0CmXqmnwDLwrAV1v",
"last4": "4242", "last4": "4242",
"metadata": {}, "metadata": {},
"name": "Ada Starr", "name": "Ada Starr",
@@ -24,8 +24,8 @@
"tokenization_method": null "tokenization_method": null
}, },
"client_ip": "107.202.144.213", "client_ip": "107.202.144.213",
"created": 1548695523, "created": 1539881152,
"id": "tok_1DxdeRGh0CmXqmnw5X6CeYnC", "id": "tok_1DMedAGh0CmXqmnwduQE6C3S",
"livemode": false, "livemode": false,
"object": "token", "object": "token",
"type": "card", "type": "card",

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,18 +1,14 @@
{ {
"account_balance": 0, "account_balance": 0,
"created": 1000000000, "created": 1539832157,
"currency": null, "currency": null,
"default_source": "card_NORMALIZED00000000000001", "default_source": "card_1DMRsvGh0CmXqmnwRFN7yRRD",
"delinquent": false, "delinquent": false,
"description": "zulip (Zulip Dev)", "description": "zulip (Zulip Dev)",
"discount": null, "discount": null,
"email": "hamlet@zulip.com", "email": "hamlet@zulip.com",
"id": "cus_NORMALIZED0001", "id": "cus_Do40UO0WhJ4ZIl",
"invoice_prefix": "NORMA01", "invoice_prefix": "7783290",
"invoice_settings": {
"custom_fields": null,
"footer": null
},
"livemode": false, "livemode": false,
"metadata": { "metadata": {
"realm_id": "1", "realm_id": "1",
@@ -33,14 +29,14 @@
"address_zip_check": "pass", "address_zip_check": "pass",
"brand": "Visa", "brand": "Visa",
"country": "US", "country": "US",
"customer": "cus_NORMALIZED0001", "customer": "cus_Do40UO0WhJ4ZIl",
"cvc_check": "pass", "cvc_check": "pass",
"dynamic_last4": null, "dynamic_last4": null,
"exp_month": 3, "exp_month": 3,
"exp_year": 2033, "exp_year": 2033,
"fingerprint": "NORMALIZED000001", "fingerprint": "6dAXT9VZvwro65EK",
"funding": "credit", "funding": "credit",
"id": "card_NORMALIZED00000000000001", "id": "card_1DMRsvGh0CmXqmnwRFN7yRRD",
"last4": "4242", "last4": "4242",
"metadata": {}, "metadata": {},
"name": "Ada Starr", "name": "Ada Starr",
@@ -51,14 +47,14 @@
"has_more": false, "has_more": false,
"object": "list", "object": "list",
"total_count": 1, "total_count": 1,
"url": "/v1/customers/cus_NORMALIZED0001/sources" "url": "/v1/customers/cus_Do40UO0WhJ4ZIl/sources"
}, },
"subscriptions": { "subscriptions": {
"data": [], "data": [],
"has_more": false, "has_more": false,
"object": "list", "object": "list",
"total_count": 0, "total_count": 0,
"url": "/v1/customers/cus_NORMALIZED0001/subscriptions" "url": "/v1/customers/cus_Do40UO0WhJ4ZIl/subscriptions"
}, },
"tax_info": null, "tax_info": null,
"tax_info_verification": null "tax_info_verification": null

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -14,18 +14,18 @@
"dynamic_last4": null, "dynamic_last4": null,
"exp_month": 3, "exp_month": 3,
"exp_year": 2033, "exp_year": 2033,
"fingerprint": "NORMALIZED000001", "fingerprint": "6dAXT9VZvwro65EK",
"funding": "credit", "funding": "credit",
"id": "card_NORMALIZED00000000000001", "id": "card_1DMRsvGh0CmXqmnwRFN7yRRD",
"last4": "4242", "last4": "4242",
"metadata": {}, "metadata": {},
"name": "Ada Starr", "name": "Ada Starr",
"object": "card", "object": "card",
"tokenization_method": null "tokenization_method": null
}, },
"client_ip": "0.0.0.0", "client_ip": "107.202.144.213",
"created": 1000000000, "created": 1539832157,
"id": "tok_NORMALIZED00000000000001", "id": "tok_1DMRsvGh0CmXqmnwO80yZKru",
"livemode": false, "livemode": false,
"object": "token", "object": "token",
"type": "card", "type": "card",

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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