mirror of
https://github.com/zulip/zulip.git
synced 2025-10-24 00:23:49 +00:00
Compare commits
185 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4a826461b | ||
|
|
e37a734bd7 | ||
|
|
b7059807ac | ||
|
|
7fb252a27c | ||
|
|
6c4791bb2f | ||
|
|
244b499200 | ||
|
|
09ccdc301b | ||
|
|
ee6ab3d15b | ||
|
|
6336322d2f | ||
|
|
6e119842bd | ||
|
|
0a3800332f | ||
|
|
9636362cbd | ||
|
|
c90fd388c8 | ||
|
|
edf1de5416 | ||
|
|
a4c2f27bd5 | ||
|
|
019d922917 | ||
|
|
64cb7b5bed | ||
|
|
41a6511924 | ||
|
|
3a2a217fa0 | ||
|
|
8e1ebede1b | ||
|
|
a7b8e4795d | ||
|
|
9167b3efd7 | ||
|
|
e84c289c0d | ||
|
|
5a9452f3e6 | ||
|
|
d68b27e39a | ||
|
|
60334f7ccf | ||
|
|
acaf5b835c | ||
|
|
6ef745675c | ||
|
|
be179b2b6b | ||
|
|
79945622bc | ||
|
|
092ecbacc6 | ||
|
|
a802f0f339 | ||
|
|
d5b8d4b17d | ||
|
|
4de73f823c | ||
|
|
39a908d350 | ||
|
|
94af282460 | ||
|
|
547ac31ee6 | ||
|
|
7c50e1a40e | ||
|
|
e2dcea1664 | ||
|
|
839c86ead5 | ||
|
|
48995f8218 | ||
|
|
276830b275 | ||
|
|
f43736bef5 | ||
|
|
49d7830886 | ||
|
|
761dae7571 | ||
|
|
d9c94944e0 | ||
|
|
3780ee2fc6 | ||
|
|
ff520a3d52 | ||
|
|
170f8dabd4 | ||
|
|
b85046eb08 | ||
|
|
ee9a450f4e | ||
|
|
26f0695b94 | ||
|
|
a30cd12433 | ||
|
|
ea0b8cc011 | ||
|
|
6b016eb5bf | ||
|
|
35b0b2fb6a | ||
|
|
05c2269571 | ||
|
|
d3048b8791 | ||
|
|
ec8a284ad5 | ||
|
|
61b5577cf4 | ||
|
|
21c5ea1444 | ||
|
|
421ce4ffe0 | ||
|
|
792748ae63 | ||
|
|
1252fbe434 | ||
|
|
d026f35c5b | ||
|
|
c7839ff084 | ||
|
|
5b4f5a89fb | ||
|
|
f54f07f6cb | ||
|
|
f1595cf7db | ||
|
|
e953ff71e4 | ||
|
|
47e2b07316 | ||
|
|
4ac849d006 | ||
|
|
43b09fd89d | ||
|
|
8a1704f765 | ||
|
|
96be7b72c2 | ||
|
|
99b37de89f | ||
|
|
323223db4c | ||
|
|
91e5ef39eb | ||
|
|
b9aa772885 | ||
|
|
a0ce536fa4 | ||
|
|
3325f2ef06 | ||
|
|
920c73a64e | ||
|
|
9c272d6ca7 | ||
|
|
7482a7e61f | ||
|
|
4c4ec78a3a | ||
|
|
62b1652b3a | ||
|
|
931d989f53 | ||
|
|
dbd0457f6e | ||
|
|
1ae5194619 | ||
|
|
4331114329 | ||
|
|
f907102842 | ||
|
|
1338cc32b4 | ||
|
|
3f265b257a | ||
|
|
a715e6aa33 | ||
|
|
291cf12c45 | ||
|
|
948171a839 | ||
|
|
8fdcadb08a | ||
|
|
2e9544a9a5 | ||
|
|
a8d6c44db4 | ||
|
|
406f9f7852 | ||
|
|
cc21dc8ec7 | ||
|
|
775d9a889c | ||
|
|
89a3a627be | ||
|
|
a03ea9dc08 | ||
|
|
252254db69 | ||
|
|
8a6ebeda87 | ||
|
|
7bb554d58b | ||
|
|
1a4214a488 | ||
|
|
ba2a359e3f | ||
|
|
201cab601a | ||
|
|
9628cc9278 | ||
|
|
97d4f71f0f | ||
|
|
a8675741a0 | ||
|
|
290b54a82d | ||
|
|
7a448010e2 | ||
|
|
8f1db3b5c5 | ||
|
|
0e26371c4b | ||
|
|
fa6f2f1776 | ||
|
|
56c770987f | ||
|
|
fc472b7b1c | ||
|
|
eb6d5346fa | ||
|
|
3349c596d5 | ||
|
|
5a58adf62a | ||
|
|
bf68d676c0 | ||
|
|
2dcabec005 | ||
|
|
ec46497507 | ||
|
|
6c18abddb6 | ||
|
|
8d9e51a067 | ||
|
|
76d1c70f7a | ||
|
|
4174047f64 | ||
|
|
adbee935f7 | ||
|
|
ebed224395 | ||
|
|
738429cf77 | ||
|
|
2f91471e98 | ||
|
|
8f9807176e | ||
|
|
217f5731fe | ||
|
|
ca0fd7f797 | ||
|
|
da4c4f74f2 | ||
|
|
b2068222e0 | ||
|
|
2dfc0463bd | ||
|
|
92c538c862 | ||
|
|
2e03e1b6ee | ||
|
|
0ac81a1b77 | ||
|
|
92e840efd1 | ||
|
|
f1a8c402d1 | ||
|
|
8878fee6d6 | ||
|
|
783f3fac3b | ||
|
|
5f397e5fa8 | ||
|
|
8ff2684d61 | ||
|
|
016f53711d | ||
|
|
e921c7dafe | ||
|
|
9b950f9c6a | ||
|
|
aab515feb9 | ||
|
|
b178bb7c59 | ||
|
|
8a783c448f | ||
|
|
f4f8d091a9 | ||
|
|
67157ec2b8 | ||
|
|
4bf4c8a040 | ||
|
|
08a844153c | ||
|
|
69e04c20f9 | ||
|
|
ec8d341cae | ||
|
|
3ee59df091 | ||
|
|
dd940d2eac | ||
|
|
e3f0c28528 | ||
|
|
b44ee89245 | ||
|
|
ee2654c4ee | ||
|
|
c12f8de80b | ||
|
|
94437ab5be | ||
|
|
a9a30ad5b4 | ||
|
|
e9f0ef4c15 | ||
|
|
2e16e27265 | ||
|
|
f5e2a2a38f | ||
|
|
5a177bff3a | ||
|
|
fabb5ffe94 | ||
|
|
5b4a673bbd | ||
|
|
afeb73e12a | ||
|
|
d5a39a6564 | ||
|
|
a4b5ee41ea | ||
|
|
884a8d5628 | ||
|
|
8c9e521f57 | ||
|
|
2f04875ad3 | ||
|
|
9dcf1944ad | ||
|
|
6b895d1622 | ||
|
|
1a65e8a538 | ||
|
|
3240a513f1 |
@@ -25,4 +25,3 @@ forin
|
||||
uper
|
||||
slac
|
||||
couldn
|
||||
ges
|
||||
|
||||
@@ -50,8 +50,6 @@
|
||||
"import/extensions": "error",
|
||||
"import/first": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-cycle": ["error", {"ignoreExternal": true}],
|
||||
"import/no-duplicates": "error",
|
||||
"import/no-self-import": "error",
|
||||
"import/no-unresolved": "off",
|
||||
"import/no-useless-path-segments": "error",
|
||||
@@ -66,6 +64,7 @@
|
||||
"no-catch-shadow": "error",
|
||||
"no-constant-condition": ["error", {"checkLoops": false}],
|
||||
"no-div-regex": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-else-return": "error",
|
||||
"no-eq-null": "error",
|
||||
"no-eval": "error",
|
||||
@@ -94,10 +93,7 @@
|
||||
"no-undef-init": "error",
|
||||
"no-unneeded-ternary": ["error", {"defaultAssignment": false}],
|
||||
"no-unused-expressions": "error",
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{"args": "all", "argsIgnorePattern": "^_", "ignoreRestSiblings": true}
|
||||
],
|
||||
"no-unused-vars": ["error", {"ignoreRestSiblings": true}],
|
||||
"no-use-before-define": ["error", {"functions": false}],
|
||||
"no-useless-concat": "error",
|
||||
"no-useless-constructor": "error",
|
||||
@@ -170,12 +166,9 @@
|
||||
},
|
||||
"rules": {
|
||||
// Disable base rule to avoid conflict
|
||||
"no-duplicate-imports": "off",
|
||||
"no-use-before-define": "off",
|
||||
|
||||
"@typescript-eslint/consistent-type-assertions": [
|
||||
"error",
|
||||
{"assertionStyle": "never"}
|
||||
],
|
||||
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
@@ -183,7 +176,9 @@
|
||||
{"allowExpressions": true}
|
||||
],
|
||||
"@typescript-eslint/member-ordering": "error",
|
||||
"@typescript-eslint/no-duplicate-imports": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-parameter-properties": "error",
|
||||
"@typescript-eslint/no-unnecessary-condition": "off",
|
||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
@@ -191,13 +186,10 @@
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{"args": "all", "argsIgnorePattern": "^_", "ignoreRestSiblings": true}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["error", {"ignoreRestSiblings": true}],
|
||||
"@typescript-eslint/no-use-before-define": ["error", {"functions": false}],
|
||||
"@typescript-eslint/parameter-properties": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"import/no-cycle": "error",
|
||||
"no-undef": "error"
|
||||
}
|
||||
},
|
||||
|
||||
24
.github/workflows/production-suite.yml
vendored
24
.github/workflows/production-suite.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
- name: Restore pnpm store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /__w/.pnpm-store
|
||||
path: ~/.local/share/pnpm/store
|
||||
key: v1-pnpm-store-focal-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- name: Restore python cache
|
||||
@@ -102,12 +102,6 @@ jobs:
|
||||
path: /tmp/production-build
|
||||
retention-days: 1
|
||||
|
||||
- name: Verify pnpm store path
|
||||
run: |
|
||||
set -x
|
||||
path="$(pnpm store path)"
|
||||
[[ "$path" == /__w/.pnpm-store/* ]]
|
||||
|
||||
- name: Generate failure report string
|
||||
id: failure_report_string
|
||||
if: ${{ failure() && github.repository == 'zulip/zulip' && github.event_name == 'push' }}
|
||||
@@ -190,6 +184,12 @@ jobs:
|
||||
sudo mkdir -p "${dirs[@]}"
|
||||
sudo chown -R github "${dirs[@]}"
|
||||
|
||||
- name: Restore pnpm store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.local/share/pnpm/store
|
||||
key: v1-pnpm-store-${{ matrix.os }}-${{ hashFiles('/tmp/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install production
|
||||
run: sudo /tmp/production-install ${{ matrix.extra-args }}
|
||||
|
||||
@@ -251,15 +251,9 @@ jobs:
|
||||
- docker_image: zulip/ci:bullseye-5.0
|
||||
name: 5.0 Version Upgrade
|
||||
os: bullseye
|
||||
- docker_image: zulip/ci:jammy-6.0
|
||||
- docker_image: zulip/ci:bullseye-6.0
|
||||
name: 6.0 Version Upgrade
|
||||
os: jammy
|
||||
- docker_image: zulip/ci:bookworm-7.0
|
||||
name: 7.0 Version Upgrade
|
||||
os: bookworm
|
||||
- docker_image: zulip/ci:bookworm-8.0
|
||||
name: 8.0 Version Upgrade
|
||||
os: bookworm
|
||||
os: bullseye
|
||||
|
||||
name: ${{ matrix.name }}
|
||||
container:
|
||||
|
||||
103
.github/workflows/zulip-ci.yml
vendored
103
.github/workflows/zulip-ci.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
- name: Restore pnpm store
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /__w/.pnpm-store
|
||||
path: ~/.local/share/pnpm/store
|
||||
key: v1-pnpm-store-${{ matrix.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- name: Restore python cache
|
||||
@@ -112,26 +112,11 @@ jobs:
|
||||
source tools/ci/activate-venv
|
||||
./tools/run-codespell
|
||||
|
||||
# We run the tests that are only run in a specific job early, so
|
||||
# that we get feedback to the developer about likely failures as
|
||||
# quickly as possible. Backend/mypy failures that aren't
|
||||
# identical across different versions are much more rare than
|
||||
# frontend linter or node test failures.
|
||||
- name: Run documentation and api tests
|
||||
if: ${{ matrix.include_documentation_tests }}
|
||||
- name: Run backend lint
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
# In CI, we only test links we control in test-documentation to avoid flakes
|
||||
./tools/test-documentation --skip-external-links
|
||||
./tools/test-help-documentation --skip-external-links
|
||||
./tools/test-api
|
||||
|
||||
- name: Run node tests
|
||||
if: ${{ matrix.include_frontend_tests }}
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
# Run the node tests first, since they're fast and deterministic
|
||||
./tools/test-js-with-node --coverage --parallel=1
|
||||
echo "Test suite is running under $(python --version)."
|
||||
./tools/lint --groups=backend --skip=gitlint,mypy # gitlint disabled because flaky
|
||||
|
||||
- name: Run frontend lint
|
||||
if: ${{ matrix.include_frontend_tests }}
|
||||
@@ -139,41 +124,10 @@ jobs:
|
||||
source tools/ci/activate-venv
|
||||
./tools/lint --groups=frontend --skip=gitlint # gitlint disabled because flaky
|
||||
|
||||
- name: Check schemas
|
||||
if: ${{ matrix.include_frontend_tests }}
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
# Check that various schemas are consistent. (is fast)
|
||||
./tools/check-schemas
|
||||
|
||||
- name: Check capitalization of strings
|
||||
if: ${{ matrix.include_frontend_tests }}
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
./manage.py makemessages --locale en
|
||||
PYTHONWARNINGS=ignore ./tools/check-capitalization --no-generate
|
||||
PYTHONWARNINGS=ignore ./tools/check-frontend-i18n --no-generate
|
||||
|
||||
- name: Run puppeteer tests
|
||||
if: ${{ matrix.include_frontend_tests }}
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
./tools/test-js-with-puppeteer
|
||||
|
||||
- name: Check pnpm dedupe
|
||||
if: ${{ matrix.include_frontend_tests }}
|
||||
run: pnpm dedupe --check
|
||||
|
||||
- name: Run backend lint
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
echo "Test suite is running under $(python --version)."
|
||||
./tools/lint --groups=backend --skip=gitlint,mypy # gitlint disabled because flaky
|
||||
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
./tools/test-backend --coverage --xml-report --no-html-report --include-webhooks --include-transaction-tests --no-cov-cleanup --ban-console-output
|
||||
./tools/test-backend --coverage --xml-report --no-html-report --include-webhooks --no-cov-cleanup --ban-console-output
|
||||
|
||||
- name: Run mypy
|
||||
run: |
|
||||
@@ -209,6 +163,47 @@ jobs:
|
||||
./scripts/lib/check-database-compatibility
|
||||
chmod 755 static/generated web/generated
|
||||
|
||||
- name: Run documentation and api tests
|
||||
if: ${{ matrix.include_documentation_tests }}
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
# In CI, we only test links we control in test-documentation to avoid flakes
|
||||
./tools/test-documentation --skip-external-links
|
||||
./tools/test-help-documentation --skip-external-links
|
||||
./tools/test-api
|
||||
|
||||
- name: Run node tests
|
||||
if: ${{ matrix.include_frontend_tests }}
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
# Run the node tests first, since they're fast and deterministic
|
||||
./tools/test-js-with-node --coverage --parallel=1
|
||||
|
||||
- name: Check schemas
|
||||
if: ${{ matrix.include_frontend_tests }}
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
# Check that various schemas are consistent. (is fast)
|
||||
./tools/check-schemas
|
||||
|
||||
- name: Check capitalization of strings
|
||||
if: ${{ matrix.include_frontend_tests }}
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
./manage.py makemessages --locale en
|
||||
PYTHONWARNINGS=ignore ./tools/check-capitalization --no-generate
|
||||
PYTHONWARNINGS=ignore ./tools/check-frontend-i18n --no-generate
|
||||
|
||||
- name: Run puppeteer tests
|
||||
if: ${{ matrix.include_frontend_tests }}
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
./tools/test-js-with-puppeteer
|
||||
|
||||
- name: Check pnpm dedupe
|
||||
if: ${{ matrix.include_frontend_tests }}
|
||||
run: pnpm dedupe --check
|
||||
|
||||
- name: Check for untracked files
|
||||
run: |
|
||||
source tools/ci/activate-venv
|
||||
@@ -247,12 +242,6 @@ jobs:
|
||||
- name: Check development database build
|
||||
run: ./tools/ci/setup-backend
|
||||
|
||||
- name: Verify pnpm store path
|
||||
run: |
|
||||
set -x
|
||||
path="$(pnpm store path)"
|
||||
[[ "$path" == /__w/.pnpm-store/* ]]
|
||||
|
||||
- name: Generate failure report string
|
||||
id: failure_report_string
|
||||
if: ${{ failure() && github.repository == 'zulip/zulip' && github.event_name == 'push' }}
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -17,10 +17,7 @@
|
||||
# See `git help ignore` for details on the format.
|
||||
|
||||
## Config files for the dev environment
|
||||
/zproject/apns-dev.pem
|
||||
/zproject/apns-dev-key.p8
|
||||
/zproject/dev-secrets.conf
|
||||
/zproject/custom_dev_settings.py
|
||||
/tools/conf.ini
|
||||
/tools/custom_provision
|
||||
/tools/droplets/conf.ini
|
||||
@@ -86,9 +83,6 @@ zulip.kdev4
|
||||
# Core dump files
|
||||
core
|
||||
|
||||
# Static generated files for landing page.
|
||||
/static/images/landing-page/hello/generated
|
||||
|
||||
## Miscellaneous
|
||||
# (Ideally this section is empty.)
|
||||
.transifexrc
|
||||
|
||||
53
.mailmap
53
.mailmap
@@ -12,34 +12,31 @@
|
||||
# # shows raw names/emails, filtered by mapped name:
|
||||
# $ git log --format='%an %ae' --author=$NAME | uniq -c
|
||||
|
||||
acrefoot <acrefoot@zulip.com> <acrefoot@alum.mit.edu>
|
||||
acrefoot <acrefoot@zulip.com> <acrefoot@dropbox.com>
|
||||
acrefoot <acrefoot@zulip.com> <acrefoot@humbughq.com>
|
||||
Adam Benesh <Adam.Benesh@gmail.com>
|
||||
acrefoot <acrefoot@zulip.com> <acrefoot@dropbox.com>
|
||||
acrefoot <acrefoot@zulip.com> <acrefoot@alum.mit.edu>
|
||||
Adam Benesh <Adam.Benesh@gmail.com> <Adam-Daniel.Benesh@t-systems.com>
|
||||
Adam Benesh <Adam.Benesh@gmail.com>
|
||||
Adarsh Tiwari <xoldyckk@gmail.com>
|
||||
Alex Vandiver <alexmv@zulip.com> <alex@chmrr.net>
|
||||
Alex Vandiver <alexmv@zulip.com> <github@chmrr.net>
|
||||
Allen Rabinovich <allenrabinovich@yahoo.com> <allenr@humbughq.com>
|
||||
Allen Rabinovich <allenrabinovich@yahoo.com> <allenr@zulip.com>
|
||||
Alya Abbott <alya@zulip.com> <2090066+alya@users.noreply.github.com>
|
||||
Alya Abbott <alya@zulip.com> <alyaabbott@elance-odesk.com>
|
||||
Aman Agrawal <amanagr@zulip.com>
|
||||
Aman Agrawal <amanagr@zulip.com> <f2016561@pilani.bits-pilani.ac.in>
|
||||
Aman Agrawal <amanagr@zulip.com>
|
||||
Anders Kaseorg <anders@zulip.com> <anders@zulipchat.com>
|
||||
Anders Kaseorg <anders@zulip.com> <andersk@mit.edu>
|
||||
aparna-bhatt <aparnabhatt2001@gmail.com> <86338542+aparna-bhatt@users.noreply.github.com>
|
||||
Aryan Bhokare <aryan1bhokare@gmail.com>
|
||||
Aryan Bhokare <aryan1bhokare@gmail.com> <92683836+aryan-bhokare@users.noreply.github.com>
|
||||
Aryan Shridhar <aryanshridhar7@gmail.com>
|
||||
Aryan Shridhar <aryanshridhar7@gmail.com> <53977614+aryanshridhar@users.noreply.github.com>
|
||||
Aryan Shridhar <aryanshridhar7@gmail.com>
|
||||
aparna-bhatt <aparnabhatt2001@gmail.com> <86338542+aparna-bhatt@users.noreply.github.com>
|
||||
Ashwat Kumar Singh <ashwat.kumarsingh.met20@itbhu.ac.in>
|
||||
Austin Riba <austin@zulip.com> <austin@m51.io>
|
||||
BIKI DAS <bikid475@gmail.com>
|
||||
Brijmohan Siyag <brijsiyag@gmail.com>
|
||||
Brock Whittaker <brock@zulipchat.com> <bjwhitta@asu.edu>
|
||||
Brock Whittaker <brock@zulipchat.com> <brock@zulipchat.org>
|
||||
Brock Whittaker <brock@zulipchat.com> <brockwhittaker@Brocks-MacBook.local>
|
||||
Brock Whittaker <brock@zulipchat.com> <brock@zulipchat.org>
|
||||
Chris Bobbe <cbobbe@zulip.com> <cbobbe@zulipchat.com>
|
||||
Chris Bobbe <cbobbe@zulip.com> <csbobbe@gmail.com>
|
||||
Danny Su <contact@dannysu.com> <opensource@emailengine.org>
|
||||
@@ -47,9 +44,8 @@ Dinesh <chdinesh1089@gmail.com>
|
||||
Dinesh <chdinesh1089@gmail.com> <chdinesh1089>
|
||||
Eeshan Garg <eeshan@zulip.com> <jerryguitarist@gmail.com>
|
||||
Eric Smith <erwsmith@gmail.com> <99841919+erwsmith@users.noreply.github.com>
|
||||
Evy Kassirer <evy@zulip.com>
|
||||
Evy Kassirer <evy@zulip.com> <evy.kassirer@gmail.com>
|
||||
Evy Kassirer <evy@zulip.com> <evykassirer@users.noreply.github.com>
|
||||
Evy Kassirer <evy.kassirer@gmail.com>
|
||||
Evy Kassirer <evy.kassirer@gmail.com> <evykassirer@users.noreply.github.com>
|
||||
Ganesh Pawar <pawarg256@gmail.com> <58626718+ganpa3@users.noreply.github.com>
|
||||
Greg Price <greg@zulip.com> <gnprice@gmail.com>
|
||||
Greg Price <greg@zulip.com> <greg@zulipchat.com>
|
||||
@@ -62,24 +58,19 @@ Jeff Arnold <jbarnold@gmail.com> <jbarnold@humbughq.com>
|
||||
Jeff Arnold <jbarnold@gmail.com> <jbarnold@zulip.com>
|
||||
Jessica McKellar <jesstess@mit.edu> <jesstess@humbughq.com>
|
||||
Jessica McKellar <jesstess@mit.edu> <jesstess@zulip.com>
|
||||
Joseph Ho <josephho678@gmail.com>
|
||||
Joseph Ho <josephho678@gmail.com> <62449508+Joelute@users.noreply.github.com>
|
||||
Julia Bichler <julia.bichler@tum.de> <74348920+juliaBichler01@users.noreply.github.com>
|
||||
Karl Stolley <karl@zulip.com> <karl@stolley.dev>
|
||||
Kevin Mehall <km@kevinmehall.net> <kevin@humbughq.com>
|
||||
Kevin Mehall <km@kevinmehall.net> <kevin@zulip.com>
|
||||
Kevin Scott <kevin.scott.98@gmail.com>
|
||||
Lalit Kumar Singh <lalitkumarsingh3716@gmail.com>
|
||||
Lalit Kumar Singh <lalitkumarsingh3716@gmail.com> <lalits01@smartek21.com>
|
||||
Lauryn Menard <lauryn@zulip.com> <63245456+laurynmm@users.noreply.github.com>
|
||||
Lauryn Menard <lauryn@zulip.com> <lauryn.menard@gmail.com>
|
||||
m-e-l-u-h-a-n <purushottam.tiwari.cd.cse19@itbhu.ac.in>
|
||||
m-e-l-u-h-a-n <purushottam.tiwari.cd.cse19@itbhu.ac.in> <pururshottam.tiwari.cd.cse19@itbhu.ac.in>
|
||||
Lauryn Menard <lauryn@zulip.com> <63245456+laurynmm@users.noreply.github.com>
|
||||
Mateusz Mandera <mateusz.mandera@zulip.com> <mateusz.mandera@protonmail.com>
|
||||
Matt Keller <matt@zulip.com>
|
||||
Matt Keller <matt@zulip.com> <m@cognusion.com>
|
||||
Nehal Sharma <bablinaneh@gmail.com>
|
||||
Nehal Sharma <bablinaneh@gmail.com> <68962290+N-Shar-ma@users.noreply.github.com>
|
||||
m-e-l-u-h-a-n <purushottam.tiwari.cd.cse19@itbhu.ac.in>
|
||||
m-e-l-u-h-a-n <purushottam.tiwari.cd.cse19@itbhu.ac.in> <pururshottam.tiwari.cd.cse19@itbhu.ac.in>
|
||||
Noble Mittal <noblemittal@outlook.com> <62551163+beingnoble03@users.noreply.github.com>
|
||||
nzai <nzaih18@gmail.com> <70953556+nzaih1999@users.noreply.github.com>
|
||||
Palash Baderia <palash.baderia@outlook.com>
|
||||
@@ -90,14 +81,13 @@ Priyam Seth <sethpriyam1@gmail.com> <b19188@students.iitmandi.ac.in>
|
||||
Ray Kraesig <rkraesig@zulip.com> <rkraesig@zulipchat.com>
|
||||
Reid Barton <rwbarton@gmail.com> <rwbarton@humbughq.com>
|
||||
Rein Zustand (rht) <rhtbot@protonmail.com>
|
||||
Rishabh Maheshwari <b20063@students.iitmandi.ac.in>
|
||||
Rishi Gupta <rishig@zulipchat.com> <rishig+git@mit.edu>
|
||||
Rishi Gupta <rishig@zulipchat.com> <rishig@kandralabs.com>
|
||||
Rishi Gupta <rishig@zulipchat.com> <rishig@users.noreply.github.com>
|
||||
Rishabh Maheshwari <b20063@students.iitmandi.ac.in>
|
||||
Rixant Rokaha <rixantrokaha@gmail.com>
|
||||
Rixant Rokaha <rixantrokaha@gmail.com> <rishantrokaha@gmail.com>
|
||||
Rixant Rokaha <rixantrokaha@gmail.com> <rrokaha@caldwell.edu>
|
||||
Rohan Gudimetla <rohan.gudimetla07@gmail.com>
|
||||
Sahil Batra <sahil@zulip.com> <35494118+sahil839@users.noreply.github.com>
|
||||
Sahil Batra <sahil@zulip.com> <sahilbatra839@gmail.com>
|
||||
Satyam Bansal <sbansal1999@gmail.com>
|
||||
@@ -105,7 +95,6 @@ Sayam Samal <samal.sayam@gmail.com>
|
||||
Scott Feeney <scott@oceanbase.org> <scott@humbughq.com>
|
||||
Scott Feeney <scott@oceanbase.org> <scott@zulip.com>
|
||||
Shlok Patel <shlokcpatel2001@gmail.com>
|
||||
Shu Chen <shu@zulip.com>
|
||||
Somesh Ranjan <somesh.ranjan.met20@itbhu.ac.in> <77766761+somesh202@users.noreply.github.com>
|
||||
Steve Howell <showell@zulip.com> <showell30@yahoo.com>
|
||||
Steve Howell <showell@zulip.com> <showell@yahoo.com>
|
||||
@@ -119,22 +108,24 @@ Tim Abbott <tabbott@zulip.com> <tabbott@humbughq.com>
|
||||
Tim Abbott <tabbott@zulip.com> <tabbott@mit.edu>
|
||||
Tim Abbott <tabbott@zulip.com> <tabbott@zulipchat.com>
|
||||
Ujjawal Modi <umodi2003@gmail.com> <99073049+Ujjawal3@users.noreply.github.com>
|
||||
umkay <ukhan@zulipchat.com> <umaimah.k@gmail.com>
|
||||
umkay <ukhan@zulipchat.com> <umkay@users.noreply.github.com>
|
||||
Viktor Illmer <1476338+v-ji@users.noreply.github.com>
|
||||
Vishnu KS <vishnu@zulip.com> <hackerkid@vishnuks.com>
|
||||
Vishnu KS <vishnu@zulip.com> <yo@vishnuks.com>
|
||||
Waseem Daher <wdaher@zulip.com> <wdaher@dropbox.com>
|
||||
Alya Abbott <alya@zulip.com> <alyaabbott@elance-odesk.com>
|
||||
umkay <ukhan@zulipchat.com> <umaimah.k@gmail.com>
|
||||
umkay <ukhan@zulipchat.com> <umkay@users.noreply.github.com>
|
||||
Waseem Daher <wdaher@zulip.com> <wdaher@humbughq.com>
|
||||
Yash RE <33805964+YashRE42@users.noreply.github.com>
|
||||
Waseem Daher <wdaher@zulip.com> <wdaher@dropbox.com>
|
||||
Yash RE <33805964+YashRE42@users.noreply.github.com> <YashRE42@github.com>
|
||||
Yash RE <33805964+YashRE42@users.noreply.github.com>
|
||||
Yogesh Sirsat <yogeshsirsat56@gmail.com>
|
||||
Yogesh Sirsat <yogeshsirsat56@gmail.com> <41695888+yogesh-sirsat@users.noreply.github.com>
|
||||
Zeeshan Equbal <equbalzeeshan@gmail.com>
|
||||
Zeeshan Equbal <equbalzeeshan@gmail.com> <54993043+zee-bit@users.noreply.github.com>
|
||||
Zeeshan Equbal <equbalzeeshan@gmail.com>
|
||||
Zev Benjamin <zev@zulip.com> <zev@dropbox.com>
|
||||
Zev Benjamin <zev@zulip.com> <zev@humbughq.com>
|
||||
Zev Benjamin <zev@zulip.com> <zev@mit.edu>
|
||||
Zixuan James Li <p359101898@gmail.com>
|
||||
Zixuan James Li <p359101898@gmail.com> <359101898@qq.com>
|
||||
Zixuan James Li <p359101898@gmail.com> <39874143+PIG208@users.noreply.github.com>
|
||||
Zixuan James Li <p359101898@gmail.com> <359101898@qq.com>
|
||||
Joseph Ho <josephho678@gmail.com>
|
||||
Joseph Ho <josephho678@gmail.com> <62449508+Joelute@users.noreply.github.com>
|
||||
|
||||
@@ -136,8 +136,7 @@ Here are some guidelines for you how can help:
|
||||
|
||||
I’ve gone ahead and moved the other copy of this message to this thread.
|
||||
|
||||
- If asked a question in a direct message that is better discussed in a public
|
||||
stream:
|
||||
- If asked a question in a PM that is better discussed in a public stream:
|
||||
> Hi @user! Please start by reviewing
|
||||
> https://zulip.com/development-community/#community-norms to learn how to
|
||||
> get help in this community.
|
||||
@@ -167,7 +166,7 @@ Here are some guidelines for you how can help:
|
||||
|
||||
- Try to assume the best intentions from others (given the range of
|
||||
possibilities presented by their visible behavior), and stick with a friendly
|
||||
and positive tone even when someone’s behavior is poor or disrespectful.
|
||||
and positive tone even when someone‘s behavior is poor or disrespectful.
|
||||
Everyone has bad days and stressful situations that can result in them
|
||||
behaving not their best, and while we should be firm about our community
|
||||
rules, we should also enforce them with kindness.
|
||||
|
||||
@@ -154,10 +154,6 @@ repository](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+label%3
|
||||
|
||||
- We especially recommend browsing recently opened issues, as there are more
|
||||
likely to be easy ones for you to find.
|
||||
- Take a look at issues with the ["good first issue"
|
||||
label](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22),
|
||||
as they are especially accessible to new contributors. However, you will
|
||||
likely find issues without this label that are accessible as well.
|
||||
- All issues are partitioned into areas like
|
||||
admin, compose, emoji, hotkeys, i18n, onboarding, search, etc. Look
|
||||
through our [list of labels](https://github.com/zulip/zulip/labels), and
|
||||
@@ -249,12 +245,6 @@ labels.
|
||||
use the existing pull request (PR) as a starting point for your contribution. If
|
||||
you think a different approach is needed, you can post a new PR, with a comment that clearly
|
||||
explains _why_ you decided to start from scratch.
|
||||
- **What if I ask if someone is still working on an issue, and they don't
|
||||
respond?** If you don't get a reply within 2-3 days, go ahead and post a comment
|
||||
that you are working on the issue, and submit a pull request. If the original
|
||||
assignee ends up submitting a pull request first, no worries! You can help by
|
||||
providing feedback on their work, or submit your own PR if you think a
|
||||
different approach is needed (as described above).
|
||||
- **Can I come up with my own feature idea and work on it?** We welcome
|
||||
suggestions of features or other improvements that you feel would be valuable. If you
|
||||
have a new feature you'd like to add, you can start a conversation [in our
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# -f ./Dockerfile-postgresql -t zulip/zulip-postgresql:14 --push .
|
||||
|
||||
# Currently the PostgreSQL images do not support automatic upgrading of
|
||||
# the on-disk data in volumes. So the base image cannot currently be upgraded
|
||||
# the on-disk data in volumes. So the base image can not currently be upgraded
|
||||
# without users needing a manual pgdump and restore.
|
||||
|
||||
# https://hub.docker.com/r/groonga/pgroonga/tags
|
||||
|
||||
@@ -17,7 +17,7 @@ Come find us on the [development community chat](https://zulip.com/development-c
|
||||
[](https://github.com/zulip/zulip/actions/workflows/zulip-ci.yml?query=branch%3Amain)
|
||||
[](https://codecov.io/gh/zulip/zulip)
|
||||
[][mypy-coverage]
|
||||
[](https://github.com/astral-sh/ruff)
|
||||
[](https://github.com/charliermarsh/ruff)
|
||||
[](https://github.com/psf/black)
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://github.com/zulip/zulip/releases/latest)
|
||||
|
||||
@@ -8,7 +8,6 @@ from django.conf import settings
|
||||
from django.db import connection, models
|
||||
from django.db.models import F
|
||||
from psycopg2.sql import SQL, Composable, Identifier, Literal
|
||||
from typing_extensions import TypeAlias, override
|
||||
|
||||
from analytics.models import (
|
||||
BaseCount,
|
||||
@@ -19,20 +18,14 @@ from analytics.models import (
|
||||
UserCount,
|
||||
installation_epoch,
|
||||
)
|
||||
from zerver.lib.logging_util import log_to_file
|
||||
from zerver.lib.timestamp import ceiling_to_day, ceiling_to_hour, floor_to_hour, verify_UTC
|
||||
from zerver.models import Message, Realm, RealmAuditLog, Stream, UserActivityInterval, UserProfile
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
from zilencer.models import (
|
||||
RemoteInstallationCount,
|
||||
RemoteRealm,
|
||||
RemoteRealmCount,
|
||||
RemoteZulipServer,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger("zulip.analytics")
|
||||
## Logging setup ##
|
||||
|
||||
logger = logging.getLogger("zulip.management")
|
||||
log_to_file(logger, settings.ANALYTICS_LOG_PATH)
|
||||
|
||||
# You can't subtract timedelta.max from a datetime, so use this instead
|
||||
TIMEDELTA_MAX = timedelta(days=365 * 1000)
|
||||
@@ -69,7 +62,6 @@ class CountStat:
|
||||
else:
|
||||
self.interval = self.time_increment
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f"<CountStat: {self.property}>"
|
||||
|
||||
@@ -109,9 +101,6 @@ class DataCollector:
|
||||
self.output_table = output_table
|
||||
self.pull_function = pull_function
|
||||
|
||||
def depends_on_realm(self) -> bool:
|
||||
return self.output_table in (UserCount, StreamCount)
|
||||
|
||||
|
||||
## CountStat-level operations ##
|
||||
|
||||
@@ -200,7 +189,7 @@ def do_fill_count_stat_at_hour(
|
||||
def do_delete_counts_at_hour(stat: CountStat, end_time: datetime) -> None:
|
||||
if isinstance(stat, LoggingCountStat):
|
||||
InstallationCount.objects.filter(property=stat.property, end_time=end_time).delete()
|
||||
if stat.data_collector.depends_on_realm():
|
||||
if stat.data_collector.output_table in [UserCount, StreamCount]:
|
||||
RealmCount.objects.filter(property=stat.property, end_time=end_time).delete()
|
||||
else:
|
||||
UserCount.objects.filter(property=stat.property, end_time=end_time).delete()
|
||||
@@ -221,7 +210,7 @@ def do_aggregate_to_summary_table(
|
||||
else:
|
||||
realm_clause = SQL("")
|
||||
|
||||
if stat.data_collector.depends_on_realm():
|
||||
if output_table in (UserCount, StreamCount):
|
||||
realmcount_query = SQL(
|
||||
"""
|
||||
INSERT INTO analytics_realmcount
|
||||
@@ -302,7 +291,7 @@ def do_aggregate_to_summary_table(
|
||||
|
||||
# called from zerver.actions; should not throw any errors
|
||||
def do_increment_logging_stat(
|
||||
model_object_for_bucket: Union[Realm, UserProfile, Stream, "RemoteRealm", "RemoteZulipServer"],
|
||||
zerver_object: Union[Realm, UserProfile, Stream],
|
||||
stat: CountStat,
|
||||
subgroup: Optional[Union[str, int, bool]],
|
||||
event_time: datetime,
|
||||
@@ -313,37 +302,21 @@ def do_increment_logging_stat(
|
||||
|
||||
table = stat.data_collector.output_table
|
||||
if table == RealmCount:
|
||||
assert isinstance(model_object_for_bucket, Realm)
|
||||
id_args: Dict[
|
||||
str, Optional[Union[Realm, UserProfile, Stream, "RemoteRealm", "RemoteZulipServer"]]
|
||||
] = {"realm": model_object_for_bucket}
|
||||
assert isinstance(zerver_object, Realm)
|
||||
id_args: Dict[str, Union[Realm, UserProfile, Stream]] = {"realm": zerver_object}
|
||||
elif table == UserCount:
|
||||
assert isinstance(model_object_for_bucket, UserProfile)
|
||||
id_args = {"realm": model_object_for_bucket.realm, "user": model_object_for_bucket}
|
||||
elif table == StreamCount:
|
||||
assert isinstance(model_object_for_bucket, Stream)
|
||||
id_args = {"realm": model_object_for_bucket.realm, "stream": model_object_for_bucket}
|
||||
elif table == RemoteInstallationCount:
|
||||
assert isinstance(model_object_for_bucket, RemoteZulipServer)
|
||||
id_args = {"server": model_object_for_bucket, "remote_id": None}
|
||||
elif table == RemoteRealmCount:
|
||||
assert isinstance(model_object_for_bucket, RemoteRealm)
|
||||
id_args = {
|
||||
"server": model_object_for_bucket.server,
|
||||
"remote_realm": model_object_for_bucket,
|
||||
"remote_id": None,
|
||||
}
|
||||
else:
|
||||
raise AssertionError("Unsupported CountStat output_table")
|
||||
assert isinstance(zerver_object, UserProfile)
|
||||
id_args = {"realm": zerver_object.realm, "user": zerver_object}
|
||||
else: # StreamCount
|
||||
assert isinstance(zerver_object, Stream)
|
||||
id_args = {"realm": zerver_object.realm, "stream": zerver_object}
|
||||
|
||||
if stat.frequency == CountStat.DAY:
|
||||
end_time = ceiling_to_day(event_time)
|
||||
elif stat.frequency == CountStat.HOUR:
|
||||
else: # CountStat.HOUR:
|
||||
end_time = ceiling_to_hour(event_time)
|
||||
else:
|
||||
raise AssertionError("Unsupported CountStat frequency")
|
||||
|
||||
row, created = table._default_manager.get_or_create(
|
||||
row, created = table.objects.get_or_create(
|
||||
property=stat.property,
|
||||
subgroup=subgroup,
|
||||
end_time=end_time,
|
||||
@@ -373,7 +346,7 @@ def do_drop_single_stat(property: str) -> None:
|
||||
|
||||
## DataCollector-level operations ##
|
||||
|
||||
QueryFn: TypeAlias = Callable[[Dict[str, Composable]], Composable]
|
||||
QueryFn = Callable[[Dict[str, Composable]], Composable]
|
||||
|
||||
|
||||
def do_pull_by_sql_query(
|
||||
@@ -473,13 +446,7 @@ def count_message_by_user_query(realm: Optional[Realm]) -> QueryFn:
|
||||
if realm is None:
|
||||
realm_clause: Composable = SQL("")
|
||||
else:
|
||||
# We limit both userprofile and message so that we only see
|
||||
# users from this realm, but also get the performance speedup
|
||||
# of limiting messages by realm.
|
||||
realm_clause = SQL(
|
||||
"zerver_userprofile.realm_id = {} AND zerver_message.realm_id = {} AND"
|
||||
).format(Literal(realm.id), Literal(realm.id))
|
||||
# Uses index: zerver_message_realm_date_sent (or the only-date index)
|
||||
realm_clause = SQL("zerver_userprofile.realm_id = {} AND").format(Literal(realm.id))
|
||||
return lambda kwargs: SQL(
|
||||
"""
|
||||
INSERT INTO analytics_usercount
|
||||
@@ -506,13 +473,7 @@ def count_message_type_by_user_query(realm: Optional[Realm]) -> QueryFn:
|
||||
if realm is None:
|
||||
realm_clause: Composable = SQL("")
|
||||
else:
|
||||
# We limit both userprofile and message so that we only see
|
||||
# users from this realm, but also get the performance speedup
|
||||
# of limiting messages by realm.
|
||||
realm_clause = SQL(
|
||||
"zerver_userprofile.realm_id = {} AND zerver_message.realm_id = {} AND"
|
||||
).format(Literal(realm.id), Literal(realm.id))
|
||||
# Uses index: zerver_message_realm_date_sent (or the only-date index)
|
||||
realm_clause = SQL("zerver_userprofile.realm_id = {} AND").format(Literal(realm.id))
|
||||
return lambda kwargs: SQL(
|
||||
"""
|
||||
INSERT INTO analytics_usercount
|
||||
@@ -561,10 +522,7 @@ def count_message_by_stream_query(realm: Optional[Realm]) -> QueryFn:
|
||||
if realm is None:
|
||||
realm_clause: Composable = SQL("")
|
||||
else:
|
||||
realm_clause = SQL(
|
||||
"zerver_stream.realm_id = {} AND zerver_message.realm_id = {} AND"
|
||||
).format(Literal(realm.id), Literal(realm.id))
|
||||
# Uses index: zerver_message_realm_date_sent (or the only-date index)
|
||||
realm_clause = SQL("zerver_stream.realm_id = {} AND").format(Literal(realm.id))
|
||||
return lambda kwargs: SQL(
|
||||
"""
|
||||
INSERT INTO analytics_streamcount
|
||||
@@ -842,12 +800,6 @@ def get_count_stats(realm: Optional[Realm] = None) -> Dict[str, CountStat]:
|
||||
CountStat(
|
||||
"minutes_active::day", DataCollector(UserCount, do_pull_minutes_active), CountStat.DAY
|
||||
),
|
||||
# Tracks the number of push notifications requested by the server.
|
||||
LoggingCountStat(
|
||||
"mobile_pushes_sent::day",
|
||||
RealmCount,
|
||||
CountStat.DAY,
|
||||
),
|
||||
# Rate limiting stats
|
||||
# Used to limit the number of invitation emails sent by a realm
|
||||
LoggingCountStat("invites_sent::day", RealmCount, CountStat.DAY),
|
||||
@@ -862,65 +814,8 @@ def get_count_stats(realm: Optional[Realm] = None) -> Dict[str, CountStat]:
|
||||
),
|
||||
]
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
# See also the remote_installation versions of these in REMOTE_INSTALLATION_COUNT_STATS.
|
||||
count_stats_.append(
|
||||
LoggingCountStat(
|
||||
"mobile_pushes_received::day",
|
||||
RemoteRealmCount,
|
||||
CountStat.DAY,
|
||||
)
|
||||
)
|
||||
count_stats_.append(
|
||||
LoggingCountStat(
|
||||
"mobile_pushes_forwarded::day",
|
||||
RemoteRealmCount,
|
||||
CountStat.DAY,
|
||||
)
|
||||
)
|
||||
|
||||
return OrderedDict((stat.property, stat) for stat in count_stats_)
|
||||
|
||||
|
||||
# These properties are tracked by the bouncer itself and therefore syncing them
|
||||
# from a remote server should not be allowed - or the server would be able to interfere
|
||||
# with our data.
|
||||
BOUNCER_ONLY_REMOTE_COUNT_STAT_PROPERTIES = [
|
||||
"mobile_pushes_received::day",
|
||||
"mobile_pushes_forwarded::day",
|
||||
]
|
||||
|
||||
# To avoid refactoring for now COUNT_STATS can be used as before
|
||||
COUNT_STATS = get_count_stats()
|
||||
|
||||
REMOTE_INSTALLATION_COUNT_STATS = OrderedDict()
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
# REMOTE_INSTALLATION_COUNT_STATS contains duplicates of the
|
||||
# RemoteRealmCount stats declared above; it is necessary because
|
||||
# pre-8.0 servers do not send the fields required to identify a
|
||||
# RemoteRealm.
|
||||
|
||||
# Tracks the number of push notifications requested to be sent
|
||||
# by a remote server.
|
||||
REMOTE_INSTALLATION_COUNT_STATS["mobile_pushes_received::day"] = LoggingCountStat(
|
||||
"mobile_pushes_received::day",
|
||||
RemoteInstallationCount,
|
||||
CountStat.DAY,
|
||||
)
|
||||
# Tracks the number of push notifications successfully sent to
|
||||
# mobile devices, as requested by the remote server. Therefore
|
||||
# this should be less than or equal to mobile_pushes_received -
|
||||
# with potential tiny offsets resulting from a request being
|
||||
# *received* by the bouncer right before midnight, but *sent* to
|
||||
# the mobile device right after midnight. This would cause the
|
||||
# increments to happen to CountStat records for different days.
|
||||
REMOTE_INSTALLATION_COUNT_STATS["mobile_pushes_forwarded::day"] = LoggingCountStat(
|
||||
"mobile_pushes_forwarded::day",
|
||||
RemoteInstallationCount,
|
||||
CountStat.DAY,
|
||||
)
|
||||
|
||||
ALL_COUNT_STATS = OrderedDict(
|
||||
list(COUNT_STATS.items()) + list(REMOTE_INSTALLATION_COUNT_STATS.items())
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from math import sqrt
|
||||
from random import Random
|
||||
from random import gauss, random, seed
|
||||
from typing import List
|
||||
|
||||
from analytics.lib.counts import CountStat
|
||||
@@ -36,8 +36,6 @@ def generate_time_series_data(
|
||||
partial_sum -- If True, return partial sum of the series.
|
||||
random_seed -- Seed for random number generator.
|
||||
"""
|
||||
rng = Random(random_seed)
|
||||
|
||||
if frequency == CountStat.HOUR:
|
||||
length = days * 24
|
||||
seasonality = [non_business_hours_base] * 24 * 7
|
||||
@@ -46,13 +44,13 @@ def generate_time_series_data(
|
||||
seasonality[24 * day + hour] = business_hours_base
|
||||
holidays = []
|
||||
for i in range(days):
|
||||
holidays.extend([rng.random() < holiday_rate] * 24)
|
||||
holidays.extend([random() < holiday_rate] * 24)
|
||||
elif frequency == CountStat.DAY:
|
||||
length = days
|
||||
seasonality = [8 * business_hours_base + 16 * non_business_hours_base] * 5 + [
|
||||
24 * non_business_hours_base
|
||||
] * 2
|
||||
holidays = [rng.random() < holiday_rate for i in range(days)]
|
||||
holidays = [random() < holiday_rate for i in range(days)]
|
||||
else:
|
||||
raise AssertionError(f"Unknown frequency: {frequency}")
|
||||
if length < 2:
|
||||
@@ -64,10 +62,11 @@ def generate_time_series_data(
|
||||
seasonality[i % len(seasonality)] * (growth_base**i) for i in range(length)
|
||||
]
|
||||
|
||||
noise_scalars = [rng.gauss(0, 1)]
|
||||
seed(random_seed)
|
||||
noise_scalars = [gauss(0, 1)]
|
||||
for i in range(1, length):
|
||||
noise_scalars.append(
|
||||
noise_scalars[-1] * autocorrelation + rng.gauss(0, 1) * (1 - autocorrelation)
|
||||
noise_scalars[-1] * autocorrelation + gauss(0, 1) * (1 - autocorrelation)
|
||||
)
|
||||
|
||||
values = [
|
||||
|
||||
@@ -30,5 +30,4 @@ def time_range(
|
||||
while current >= start:
|
||||
times.append(current)
|
||||
current -= step
|
||||
times.reverse()
|
||||
return times
|
||||
return list(reversed(times))
|
||||
|
||||
@@ -5,9 +5,8 @@ from typing import Any, Dict
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import override
|
||||
|
||||
from analytics.lib.counts import ALL_COUNT_STATS, CountStat
|
||||
from analytics.lib.counts import COUNT_STATS, CountStat
|
||||
from analytics.models import installation_epoch
|
||||
from zerver.lib.timestamp import TimeZoneNotUTCError, floor_to_day, floor_to_hour, verify_UTC
|
||||
from zerver.models import Realm
|
||||
@@ -25,7 +24,6 @@ class Command(BaseCommand):
|
||||
|
||||
Run as a cron job that runs every hour."""
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
fill_state = self.get_fill_state()
|
||||
status = fill_state["status"]
|
||||
@@ -44,7 +42,7 @@ class Command(BaseCommand):
|
||||
|
||||
warning_unfilled_properties = []
|
||||
critical_unfilled_properties = []
|
||||
for property, stat in ALL_COUNT_STATS.items():
|
||||
for property, stat in COUNT_STATS.items():
|
||||
last_fill = stat.last_successful_fill()
|
||||
if last_fill is None:
|
||||
last_fill = installation_epoch()
|
||||
|
||||
@@ -2,7 +2,6 @@ from argparse import ArgumentParser
|
||||
from typing import Any
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from typing_extensions import override
|
||||
|
||||
from analytics.lib.counts import do_drop_all_analytics_tables
|
||||
|
||||
@@ -10,11 +9,9 @@ from analytics.lib.counts import do_drop_all_analytics_tables
|
||||
class Command(BaseCommand):
|
||||
help = """Clear analytics tables."""
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: ArgumentParser) -> None:
|
||||
parser.add_argument("--force", action="store_true", help="Clear analytics tables.")
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
if options["force"]:
|
||||
do_drop_all_analytics_tables()
|
||||
|
||||
@@ -2,23 +2,20 @@ from argparse import ArgumentParser
|
||||
from typing import Any
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from typing_extensions import override
|
||||
|
||||
from analytics.lib.counts import ALL_COUNT_STATS, do_drop_single_stat
|
||||
from analytics.lib.counts import COUNT_STATS, do_drop_single_stat
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """Clear analytics tables."""
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: ArgumentParser) -> None:
|
||||
parser.add_argument("--force", action="store_true", help="Actually do it.")
|
||||
parser.add_argument("--property", help="The property of the stat to be cleared.")
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
property = options["property"]
|
||||
if property not in ALL_COUNT_STATS:
|
||||
if property not in COUNT_STATS:
|
||||
raise CommandError(f"Invalid property: {property}")
|
||||
if not options["force"]:
|
||||
raise CommandError("No action taken. Use --force.")
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Any, Dict, List, Mapping, Type, Union
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import TypeAlias, override
|
||||
|
||||
from analytics.lib.counts import COUNT_STATS, CountStat, do_drop_all_analytics_tables
|
||||
from analytics.lib.fixtures import generate_time_series_data
|
||||
@@ -35,7 +34,6 @@ from zerver.models import (
|
||||
UserGroup,
|
||||
UserProfile,
|
||||
)
|
||||
from zerver.models.groups import SystemGroups
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -69,7 +67,6 @@ class Command(BaseCommand):
|
||||
random_seed=self.random_seed,
|
||||
)
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
# TODO: This should arguably only delete the objects
|
||||
# associated with the "analytics" realm.
|
||||
@@ -115,7 +112,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
administrators_user_group = UserGroup.objects.get(
|
||||
name=SystemGroups.ADMINISTRATORS, realm=realm, is_system_group=True
|
||||
name=UserGroup.ADMINISTRATORS_GROUP_NAME, realm=realm, is_system_group=True
|
||||
)
|
||||
stream = Stream.objects.create(
|
||||
name="all",
|
||||
@@ -150,7 +147,7 @@ class Command(BaseCommand):
|
||||
with open(IMAGE_FILE_PATH, "rb") as fp:
|
||||
upload_message_attachment_from_request(UploadedFile(fp), shylock, file_size)
|
||||
|
||||
FixtureData: TypeAlias = Mapping[Union[str, int, None], List[int]]
|
||||
FixtureData = Mapping[Union[str, int, None], List[int]]
|
||||
|
||||
def insert_fixture_data(
|
||||
stat: CountStat,
|
||||
@@ -170,7 +167,7 @@ class Command(BaseCommand):
|
||||
id_args = {"stream": stream, "realm": realm}
|
||||
|
||||
for subgroup, values in fixture_data.items():
|
||||
table._default_manager.bulk_create(
|
||||
table.objects.bulk_create(
|
||||
table(
|
||||
property=stat.property,
|
||||
subgroup=subgroup,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import hashlib
|
||||
import os
|
||||
import time
|
||||
from argparse import ArgumentParser
|
||||
@@ -9,11 +8,10 @@ from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import override
|
||||
|
||||
from analytics.lib.counts import ALL_COUNT_STATS, logger, process_count_stat
|
||||
from analytics.lib.counts import COUNT_STATS, logger, process_count_stat
|
||||
from scripts.lib.zulip_tools import ENDC, WARNING
|
||||
from zerver.lib.remote_server import send_server_data_to_push_bouncer
|
||||
from zerver.lib.remote_server import send_analytics_to_remote_server
|
||||
from zerver.lib.timestamp import floor_to_hour
|
||||
from zerver.models import Realm
|
||||
|
||||
@@ -23,7 +21,6 @@ class Command(BaseCommand):
|
||||
|
||||
Run as a cron job that runs every hour."""
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--time",
|
||||
@@ -40,7 +37,6 @@ class Command(BaseCommand):
|
||||
"--verbose", action="store_true", help="Print timing information to stdout."
|
||||
)
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
try:
|
||||
os.mkdir(settings.ANALYTICS_LOCK_DIR)
|
||||
@@ -75,9 +71,9 @@ class Command(BaseCommand):
|
||||
fill_to_time = floor_to_hour(fill_to_time.astimezone(timezone.utc))
|
||||
|
||||
if options["stat"] is not None:
|
||||
stats = [ALL_COUNT_STATS[options["stat"]]]
|
||||
stats = [COUNT_STATS[options["stat"]]]
|
||||
else:
|
||||
stats = list(ALL_COUNT_STATS.values())
|
||||
stats = list(COUNT_STATS.values())
|
||||
|
||||
logger.info("Starting updating analytics counts through %s", fill_to_time)
|
||||
if options["verbose"]:
|
||||
@@ -96,14 +92,5 @@ class Command(BaseCommand):
|
||||
)
|
||||
logger.info("Finished updating analytics counts through %s", fill_to_time)
|
||||
|
||||
if settings.PUSH_NOTIFICATION_BOUNCER_URL:
|
||||
# Skew 0-10 minutes based on a hash of settings.ZULIP_ORG_ID, so
|
||||
# that each server will report in at a somewhat consistent time.
|
||||
assert settings.ZULIP_ORG_ID
|
||||
delay = int.from_bytes(
|
||||
hashlib.sha256(settings.ZULIP_ORG_ID.encode()).digest(), byteorder="big"
|
||||
) % (60 * 10)
|
||||
logger.info("Sleeping %d seconds before reporting...", delay)
|
||||
time.sleep(delay)
|
||||
|
||||
send_server_data_to_push_bouncer(consider_usage_statistics=True)
|
||||
if settings.PUSH_NOTIFICATION_BOUNCER_URL and settings.SUBMIT_USAGE_STATISTICS:
|
||||
send_analytics_to_remote_server()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Generated by Django 1.10.5 on 2017-02-01 22:28
|
||||
from django.db import migrations, models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -9,25 +9,16 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="realmcount",
|
||||
index=models.Index(
|
||||
fields=["property", "end_time"],
|
||||
name="analytics_realmcount_property_end_time_3b60396b_idx",
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name="realmcount",
|
||||
index_together={("property", "end_time")},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="streamcount",
|
||||
index=models.Index(
|
||||
fields=["property", "realm", "end_time"],
|
||||
name="analytics_streamcount_property_realm_id_end_time_155ae930_idx",
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name="streamcount",
|
||||
index_together={("property", "realm", "end_time")},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="usercount",
|
||||
index=models.Index(
|
||||
fields=["property", "realm", "end_time"],
|
||||
name="analytics_usercount_property_realm_id_end_time_591dbec1_idx",
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name="usercount",
|
||||
index_together={("property", "realm", "end_time")},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("analytics", "0016_unique_constraint_when_subgroup_null"),
|
||||
]
|
||||
|
||||
# If the server was installed between 7.0 and 7.4 (or main between
|
||||
# 2c20028aa451 and 7807bff52635), it contains indexes which (when
|
||||
# running 7.5 or 7807bff52635 or higher) are never used, because
|
||||
# they contain an improper cast
|
||||
# (https://code.djangoproject.com/ticket/34840).
|
||||
#
|
||||
# We regenerate the indexes here, by dropping and re-creating
|
||||
# them, so that we know that they are properly formed.
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="installationcount",
|
||||
name="unique_installation_count",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="installationcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(subgroup__isnull=False),
|
||||
fields=("property", "subgroup", "end_time"),
|
||||
name="unique_installation_count",
|
||||
),
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name="installationcount",
|
||||
name="unique_installation_count_null_subgroup",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="installationcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(subgroup__isnull=True),
|
||||
fields=("property", "end_time"),
|
||||
name="unique_installation_count_null_subgroup",
|
||||
),
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name="realmcount",
|
||||
name="unique_realm_count",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="realmcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(subgroup__isnull=False),
|
||||
fields=("realm", "property", "subgroup", "end_time"),
|
||||
name="unique_realm_count",
|
||||
),
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name="realmcount",
|
||||
name="unique_realm_count_null_subgroup",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="realmcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(subgroup__isnull=True),
|
||||
fields=("realm", "property", "end_time"),
|
||||
name="unique_realm_count_null_subgroup",
|
||||
),
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name="streamcount",
|
||||
name="unique_stream_count",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="streamcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(subgroup__isnull=False),
|
||||
fields=("stream", "property", "subgroup", "end_time"),
|
||||
name="unique_stream_count",
|
||||
),
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name="streamcount",
|
||||
name="unique_stream_count_null_subgroup",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="streamcount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(subgroup__isnull=True),
|
||||
fields=("stream", "property", "end_time"),
|
||||
name="unique_stream_count_null_subgroup",
|
||||
),
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name="usercount",
|
||||
name="unique_user_count",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="usercount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(subgroup__isnull=False),
|
||||
fields=("user", "property", "subgroup", "end_time"),
|
||||
name="unique_user_count",
|
||||
),
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name="usercount",
|
||||
name="unique_user_count_null_subgroup",
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="usercount",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(subgroup__isnull=True),
|
||||
fields=("user", "property", "end_time"),
|
||||
name="unique_user_count_null_subgroup",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,11 +1,7 @@
|
||||
# https://github.com/typeddjango/django-stubs/issues/1698
|
||||
# mypy: disable-error-code="explicit-override"
|
||||
|
||||
from datetime import datetime
|
||||
import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q, UniqueConstraint
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.timestamp import floor_to_day
|
||||
from zerver.models import Realm, Stream, UserProfile
|
||||
@@ -20,14 +16,13 @@ class FillState(models.Model):
|
||||
STARTED = 2
|
||||
state = models.PositiveSmallIntegerField()
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.property} {self.end_time} {self.state}"
|
||||
|
||||
|
||||
# The earliest/starting end_time in FillState
|
||||
# We assume there is at least one realm
|
||||
def installation_epoch() -> datetime:
|
||||
def installation_epoch() -> datetime.datetime:
|
||||
earliest_realm_creation = Realm.objects.aggregate(models.Min("date_created"))[
|
||||
"date_created__min"
|
||||
]
|
||||
@@ -63,7 +58,6 @@ class InstallationCount(BaseCount):
|
||||
),
|
||||
]
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.property} {self.subgroup} {self.value}"
|
||||
|
||||
@@ -85,14 +79,8 @@ class RealmCount(BaseCount):
|
||||
name="unique_realm_count_null_subgroup",
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["property", "end_time"],
|
||||
name="analytics_realmcount_property_end_time_3b60396b_idx",
|
||||
)
|
||||
]
|
||||
index_together = ["property", "end_time"]
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.realm!r} {self.property} {self.subgroup} {self.value}"
|
||||
|
||||
@@ -117,14 +105,8 @@ class UserCount(BaseCount):
|
||||
]
|
||||
# This index dramatically improves the performance of
|
||||
# aggregating from users to realms
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["property", "realm", "end_time"],
|
||||
name="analytics_usercount_property_realm_id_end_time_591dbec1_idx",
|
||||
)
|
||||
]
|
||||
index_together = ["property", "realm", "end_time"]
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.user!r} {self.property} {self.subgroup} {self.value}"
|
||||
|
||||
@@ -149,13 +131,7 @@ class StreamCount(BaseCount):
|
||||
]
|
||||
# This index dramatically improves the performance of
|
||||
# aggregating from streams to realms
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["property", "realm", "end_time"],
|
||||
name="analytics_streamcount_property_realm_id_end_time_155ae930_idx",
|
||||
)
|
||||
]
|
||||
index_together = ["property", "realm", "end_time"]
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.stream!r} {self.property} {self.subgroup} {self.value} {self.id}"
|
||||
|
||||
@@ -1,80 +1,9 @@
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from unittest import mock
|
||||
|
||||
from django.utils.timezone import now as timezone_now
|
||||
|
||||
from corporate.lib.stripe import add_months
|
||||
from corporate.models import Customer, CustomerPlan, LicenseLedger
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.models import Client, UserActivity, UserProfile
|
||||
from zilencer.models import (
|
||||
RemoteRealmAuditLog,
|
||||
RemoteZulipServer,
|
||||
get_remote_server_guest_and_non_guest_count,
|
||||
)
|
||||
|
||||
event_time = timezone_now() - timedelta(days=3)
|
||||
data_list = [
|
||||
{
|
||||
"server_id": 1,
|
||||
"realm_id": 1,
|
||||
"event_type": RemoteRealmAuditLog.USER_CREATED,
|
||||
"event_time": event_time,
|
||||
"extra_data": {
|
||||
RemoteRealmAuditLog.ROLE_COUNT: {
|
||||
RemoteRealmAuditLog.ROLE_COUNT_HUMANS: {
|
||||
UserProfile.ROLE_REALM_ADMINISTRATOR: 10,
|
||||
UserProfile.ROLE_REALM_OWNER: 10,
|
||||
UserProfile.ROLE_MODERATOR: 10,
|
||||
UserProfile.ROLE_MEMBER: 10,
|
||||
UserProfile.ROLE_GUEST: 10,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"server_id": 1,
|
||||
"realm_id": 1,
|
||||
"event_type": RemoteRealmAuditLog.USER_ROLE_CHANGED,
|
||||
"event_time": event_time,
|
||||
"extra_data": {
|
||||
RemoteRealmAuditLog.ROLE_COUNT: {
|
||||
RemoteRealmAuditLog.ROLE_COUNT_HUMANS: {
|
||||
UserProfile.ROLE_REALM_ADMINISTRATOR: 20,
|
||||
UserProfile.ROLE_REALM_OWNER: 0,
|
||||
UserProfile.ROLE_MODERATOR: 0,
|
||||
UserProfile.ROLE_MEMBER: 20,
|
||||
UserProfile.ROLE_GUEST: 10,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"server_id": 1,
|
||||
"realm_id": 2,
|
||||
"event_type": RemoteRealmAuditLog.USER_CREATED,
|
||||
"event_time": event_time,
|
||||
"extra_data": {
|
||||
RemoteRealmAuditLog.ROLE_COUNT: {
|
||||
RemoteRealmAuditLog.ROLE_COUNT_HUMANS: {
|
||||
UserProfile.ROLE_REALM_ADMINISTRATOR: 10,
|
||||
UserProfile.ROLE_REALM_OWNER: 10,
|
||||
UserProfile.ROLE_MODERATOR: 0,
|
||||
UserProfile.ROLE_MEMBER: 10,
|
||||
UserProfile.ROLE_GUEST: 5,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"server_id": 1,
|
||||
"realm_id": 2,
|
||||
"event_type": RemoteRealmAuditLog.USER_CREATED,
|
||||
"event_time": event_time,
|
||||
"extra_data": {},
|
||||
},
|
||||
]
|
||||
from zerver.models import Client, UserActivity, UserProfile, flush_per_request_caches
|
||||
|
||||
|
||||
class ActivityTest(ZulipTestCase):
|
||||
@@ -102,57 +31,18 @@ class ActivityTest(ZulipTestCase):
|
||||
user_profile.is_staff = True
|
||||
user_profile.save(update_fields=["is_staff"])
|
||||
|
||||
with self.assert_database_query_count(11):
|
||||
flush_per_request_caches()
|
||||
with self.assert_database_query_count(18):
|
||||
result = self.client_get("/activity")
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
# Add data for remote activity page
|
||||
RemoteRealmAuditLog.objects.bulk_create([RemoteRealmAuditLog(**data) for data in data_list])
|
||||
remote_server = RemoteZulipServer.objects.get(id=1)
|
||||
customer = Customer.objects.create(remote_server=remote_server)
|
||||
plan = CustomerPlan.objects.create(
|
||||
customer=customer,
|
||||
billing_cycle_anchor=timezone_now(),
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL,
|
||||
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||
price_per_license=8000,
|
||||
next_invoice_date=add_months(timezone_now(), 12),
|
||||
)
|
||||
LicenseLedger.objects.create(
|
||||
licenses=10,
|
||||
licenses_at_next_renewal=10,
|
||||
event_time=timezone_now(),
|
||||
is_renewal=True,
|
||||
plan=plan,
|
||||
)
|
||||
RemoteZulipServer.objects.create(
|
||||
uuid=str(uuid.uuid4()),
|
||||
api_key="magic_secret_api_key",
|
||||
hostname="demo.example.com",
|
||||
contact_email="email@example.com",
|
||||
)
|
||||
with self.assert_database_query_count(10):
|
||||
result = self.client_get("/activity/remote")
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
with self.assert_database_query_count(4):
|
||||
result = self.client_get("/activity/integrations")
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
flush_per_request_caches()
|
||||
with self.assert_database_query_count(8):
|
||||
result = self.client_get("/realm_activity/zulip/")
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
iago = self.example_user("iago")
|
||||
flush_per_request_caches()
|
||||
with self.assert_database_query_count(5):
|
||||
result = self.client_get(f"/user_activity/{iago.id}/")
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_get_remote_server_guest_and_non_guest_count(self) -> None:
|
||||
RemoteRealmAuditLog.objects.bulk_create([RemoteRealmAuditLog(**data) for data in data_list])
|
||||
|
||||
remote_server_counts = get_remote_server_guest_and_non_guest_count(
|
||||
server_id=1, event_time=timezone_now()
|
||||
)
|
||||
self.assertEqual(remote_server_counts.non_guest_user_count, 70)
|
||||
self.assertEqual(remote_server_counts.guest_user_count, 15)
|
||||
|
||||
@@ -3,14 +3,11 @@ from typing import Any, Dict, List, Optional, Tuple, Type
|
||||
from unittest import mock
|
||||
|
||||
import orjson
|
||||
import time_machine
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.test import override_settings
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from psycopg2.sql import SQL, Literal
|
||||
from typing_extensions import override
|
||||
|
||||
from analytics.lib.counts import (
|
||||
COUNT_STATS,
|
||||
@@ -55,13 +52,8 @@ from zerver.actions.user_activity import update_user_activity_interval
|
||||
from zerver.actions.users import do_deactivate_user
|
||||
from zerver.lib.create_user import create_user
|
||||
from zerver.lib.exceptions import InvitationError
|
||||
from zerver.lib.push_notifications import (
|
||||
get_message_payload_apns,
|
||||
get_message_payload_gcm,
|
||||
hex_to_b64,
|
||||
)
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.timestamp import TimeZoneNotUTCError, ceiling_to_day, floor_to_day
|
||||
from zerver.lib.timestamp import TimeZoneNotUTCError, floor_to_day
|
||||
from zerver.lib.topic import DB_TOPIC_NAME
|
||||
from zerver.lib.utils import assert_is_not_none
|
||||
from zerver.models import (
|
||||
@@ -76,19 +68,10 @@ from zerver.models import (
|
||||
UserActivityInterval,
|
||||
UserGroup,
|
||||
UserProfile,
|
||||
get_client,
|
||||
get_user,
|
||||
is_cross_realm_bot_email,
|
||||
)
|
||||
from zerver.models.clients import get_client
|
||||
from zerver.models.groups import SystemGroups
|
||||
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||
from zerver.models.users import get_user, is_cross_realm_bot_email
|
||||
from zilencer.models import (
|
||||
RemoteInstallationCount,
|
||||
RemotePushDeviceToken,
|
||||
RemoteRealm,
|
||||
RemoteRealmCount,
|
||||
RemoteZulipServer,
|
||||
)
|
||||
from zilencer.views import get_last_id_from_server
|
||||
|
||||
|
||||
class AnalyticsTestCase(ZulipTestCase):
|
||||
@@ -98,16 +81,13 @@ class AnalyticsTestCase(ZulipTestCase):
|
||||
TIME_ZERO = datetime(1988, 3, 14, tzinfo=timezone.utc)
|
||||
TIME_LAST_HOUR = TIME_ZERO - HOUR
|
||||
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.default_realm = do_create_realm(
|
||||
string_id="realmtest", name="Realm Test", date_created=self.TIME_ZERO - 2 * self.DAY
|
||||
)
|
||||
self.administrators_user_group = UserGroup.objects.get(
|
||||
name=SystemGroups.ADMINISTRATORS,
|
||||
realm=self.default_realm,
|
||||
is_system_group=True,
|
||||
name=UserGroup.ADMINISTRATORS_GROUP_NAME, realm=self.default_realm, is_system_group=True
|
||||
)
|
||||
|
||||
# used to generate unique names in self.create_*
|
||||
@@ -115,10 +95,6 @@ class AnalyticsTestCase(ZulipTestCase):
|
||||
# used as defaults in self.assert_table_count
|
||||
self.current_property: Optional[str] = None
|
||||
|
||||
# Delete RemoteRealm registrations to have a clean slate - the relevant
|
||||
# tests want to construct this from scratch.
|
||||
RemoteRealm.objects.all().delete()
|
||||
|
||||
# Lightweight creation of users, streams, and messages
|
||||
def create_user(self, **kwargs: Any) -> UserProfile:
|
||||
self.name_counter += 1
|
||||
@@ -133,7 +109,7 @@ class AnalyticsTestCase(ZulipTestCase):
|
||||
for key, value in defaults.items():
|
||||
kwargs[key] = kwargs.get(key, value)
|
||||
kwargs["delivery_email"] = kwargs["email"]
|
||||
with time_machine.travel(kwargs["date_joined"], tick=False):
|
||||
with mock.patch("zerver.lib.create_user.timezone_now", return_value=kwargs["date_joined"]):
|
||||
pass_kwargs: Dict[str, Any] = {}
|
||||
if kwargs["is_bot"]:
|
||||
pass_kwargs["bot_type"] = UserProfile.DEFAULT_BOT
|
||||
@@ -206,9 +182,7 @@ class AnalyticsTestCase(ZulipTestCase):
|
||||
) -> None:
|
||||
if property is None:
|
||||
property = self.current_property
|
||||
queryset = table._default_manager.filter(property=property, end_time=end_time).filter(
|
||||
**kwargs
|
||||
)
|
||||
queryset = table.objects.filter(property=property, end_time=end_time).filter(**kwargs)
|
||||
if table is not InstallationCount:
|
||||
if realm is None:
|
||||
realm = self.default_realm
|
||||
@@ -253,18 +227,15 @@ class AnalyticsTestCase(ZulipTestCase):
|
||||
kwargs[arg_keys[i]] = values[i]
|
||||
for key, value in defaults.items():
|
||||
kwargs[key] = kwargs.get(key, value)
|
||||
if (
|
||||
table not in [InstallationCount, RemoteInstallationCount, RemoteRealmCount]
|
||||
and "realm" not in kwargs
|
||||
):
|
||||
if table is not InstallationCount and "realm" not in kwargs:
|
||||
if "user" in kwargs:
|
||||
kwargs["realm"] = kwargs["user"].realm
|
||||
elif "stream" in kwargs:
|
||||
kwargs["realm"] = kwargs["stream"].realm
|
||||
else:
|
||||
kwargs["realm"] = self.default_realm
|
||||
self.assertEqual(table._default_manager.filter(**kwargs).count(), 1)
|
||||
self.assert_length(arg_values, table._default_manager.count())
|
||||
self.assertEqual(table.objects.filter(**kwargs).count(), 1)
|
||||
self.assert_length(arg_values, table.objects.count())
|
||||
|
||||
|
||||
class TestProcessCountStat(AnalyticsTestCase):
|
||||
@@ -482,7 +453,6 @@ class TestProcessCountStat(AnalyticsTestCase):
|
||||
|
||||
|
||||
class TestCountStats(AnalyticsTestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
# This tests two things for each of the queries/CountStats: Handling
|
||||
@@ -687,7 +657,7 @@ class TestCountStats(AnalyticsTestCase):
|
||||
self.create_message(user1, recipient_huddle1)
|
||||
self.create_message(user2, recipient_huddle2)
|
||||
|
||||
# direct messages
|
||||
# private messages
|
||||
recipient_user1 = Recipient.objects.get(type_id=user1.id, type=Recipient.PERSONAL)
|
||||
recipient_user2 = Recipient.objects.get(type_id=user2.id, type=Recipient.PERSONAL)
|
||||
recipient_user3 = Recipient.objects.get(type_id=user3.id, type=Recipient.PERSONAL)
|
||||
@@ -1397,252 +1367,6 @@ class TestLoggingCountStats(AnalyticsTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
|
||||
def test_mobile_pushes_received_count(self) -> None:
|
||||
self.server_uuid = "6cde5f7a-1f7e-4978-9716-49f69ebfc9fe"
|
||||
self.server = RemoteZulipServer.objects.create(
|
||||
uuid=self.server_uuid,
|
||||
api_key="magic_secret_api_key",
|
||||
hostname="demo.example.com",
|
||||
last_updated=timezone_now(),
|
||||
)
|
||||
|
||||
hamlet = self.example_user("hamlet")
|
||||
token = "aaaa"
|
||||
|
||||
RemotePushDeviceToken.objects.create(
|
||||
kind=RemotePushDeviceToken.GCM,
|
||||
token=hex_to_b64(token),
|
||||
user_uuid=(hamlet.uuid),
|
||||
server=self.server,
|
||||
)
|
||||
RemotePushDeviceToken.objects.create(
|
||||
kind=RemotePushDeviceToken.GCM,
|
||||
token=hex_to_b64(token + "aa"),
|
||||
user_uuid=(hamlet.uuid),
|
||||
server=self.server,
|
||||
)
|
||||
RemotePushDeviceToken.objects.create(
|
||||
kind=RemotePushDeviceToken.APNS,
|
||||
token=hex_to_b64(token),
|
||||
user_uuid=str(hamlet.uuid),
|
||||
server=self.server,
|
||||
)
|
||||
|
||||
message = Message(
|
||||
sender=hamlet,
|
||||
recipient=self.example_user("othello").recipient,
|
||||
realm_id=hamlet.realm_id,
|
||||
content="This is test content",
|
||||
rendered_content="This is test content",
|
||||
date_sent=timezone_now(),
|
||||
sending_client=get_client("test"),
|
||||
)
|
||||
message.set_topic_name("Test topic")
|
||||
message.save()
|
||||
gcm_payload, gcm_options = get_message_payload_gcm(hamlet, message)
|
||||
apns_payload = get_message_payload_apns(
|
||||
hamlet, message, NotificationTriggers.DIRECT_MESSAGE
|
||||
)
|
||||
|
||||
# First we'll make a request without providing realm_uuid. That means
|
||||
# the bouncer can't increment the RemoteRealmCount stat, and only
|
||||
# RemoteInstallationCount will be incremented.
|
||||
payload = {
|
||||
"user_id": hamlet.id,
|
||||
"user_uuid": str(hamlet.uuid),
|
||||
"gcm_payload": gcm_payload,
|
||||
"apns_payload": apns_payload,
|
||||
"gcm_options": gcm_options,
|
||||
}
|
||||
now = timezone_now()
|
||||
with time_machine.travel(now, tick=False), mock.patch(
|
||||
"zilencer.views.send_android_push_notification", return_value=1
|
||||
), mock.patch("zilencer.views.send_apple_push_notification", return_value=1), mock.patch(
|
||||
"corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses",
|
||||
return_value=10,
|
||||
), self.assertLogs(
|
||||
"zilencer.views", level="INFO"
|
||||
):
|
||||
result = self.uuid_post(
|
||||
self.server_uuid,
|
||||
"/api/v1/remotes/push/notify",
|
||||
payload,
|
||||
content_type="application/json",
|
||||
subdomain="",
|
||||
)
|
||||
self.assert_json_success(result)
|
||||
|
||||
# There are 3 devices we created for the user:
|
||||
# 1. The mobile_pushes_received increment should match that number.
|
||||
# 2. mobile_pushes_forwarded only counts successful deliveries, and we've set up
|
||||
# the mocks above to simulate 1 successful android and 1 successful apple delivery.
|
||||
# Thus the increment should be just 2.
|
||||
self.assertTableState(
|
||||
RemoteInstallationCount,
|
||||
["property", "value", "subgroup", "server", "remote_id", "end_time"],
|
||||
[
|
||||
[
|
||||
"mobile_pushes_received::day",
|
||||
3,
|
||||
None,
|
||||
self.server,
|
||||
None,
|
||||
ceiling_to_day(now),
|
||||
],
|
||||
[
|
||||
"mobile_pushes_forwarded::day",
|
||||
2,
|
||||
None,
|
||||
self.server,
|
||||
None,
|
||||
ceiling_to_day(now),
|
||||
],
|
||||
],
|
||||
)
|
||||
self.assertFalse(
|
||||
RemoteRealmCount.objects.filter(property="mobile_pushes_received::day").exists()
|
||||
)
|
||||
self.assertFalse(
|
||||
RemoteRealmCount.objects.filter(property="mobile_pushes_forwarded::day").exists()
|
||||
)
|
||||
|
||||
# Now provide the realm_uuid. However, the RemoteRealm record doesn't exist yet, so it'll
|
||||
# still be ignored.
|
||||
payload = {
|
||||
"user_id": hamlet.id,
|
||||
"user_uuid": str(hamlet.uuid),
|
||||
"realm_uuid": str(hamlet.realm.uuid),
|
||||
"gcm_payload": gcm_payload,
|
||||
"apns_payload": apns_payload,
|
||||
"gcm_options": gcm_options,
|
||||
}
|
||||
with time_machine.travel(now, tick=False), mock.patch(
|
||||
"zilencer.views.send_android_push_notification", return_value=1
|
||||
), mock.patch("zilencer.views.send_apple_push_notification", return_value=1), mock.patch(
|
||||
"corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses",
|
||||
return_value=10,
|
||||
), self.assertLogs(
|
||||
"zilencer.views", level="INFO"
|
||||
):
|
||||
result = self.uuid_post(
|
||||
self.server_uuid,
|
||||
"/api/v1/remotes/push/notify",
|
||||
payload,
|
||||
content_type="application/json",
|
||||
subdomain="",
|
||||
)
|
||||
self.assert_json_success(result)
|
||||
|
||||
# The RemoteInstallationCount records get incremented again, but the RemoteRealmCount
|
||||
# remains ignored due to missing RemoteRealm record.
|
||||
self.assertTableState(
|
||||
RemoteInstallationCount,
|
||||
["property", "value", "subgroup", "server", "remote_id", "end_time"],
|
||||
[
|
||||
[
|
||||
"mobile_pushes_received::day",
|
||||
6,
|
||||
None,
|
||||
self.server,
|
||||
None,
|
||||
ceiling_to_day(now),
|
||||
],
|
||||
[
|
||||
"mobile_pushes_forwarded::day",
|
||||
4,
|
||||
None,
|
||||
self.server,
|
||||
None,
|
||||
ceiling_to_day(now),
|
||||
],
|
||||
],
|
||||
)
|
||||
self.assertFalse(
|
||||
RemoteRealmCount.objects.filter(property="mobile_pushes_received::day").exists()
|
||||
)
|
||||
self.assertFalse(
|
||||
RemoteRealmCount.objects.filter(property="mobile_pushes_forwarded::day").exists()
|
||||
)
|
||||
|
||||
# Create the RemoteRealm registration and repeat the above. This time RemoteRealmCount
|
||||
# stats should be collected.
|
||||
realm = hamlet.realm
|
||||
remote_realm = RemoteRealm.objects.create(
|
||||
server=self.server,
|
||||
uuid=realm.uuid,
|
||||
uuid_owner_secret=realm.uuid_owner_secret,
|
||||
host=realm.host,
|
||||
realm_deactivated=realm.deactivated,
|
||||
realm_date_created=realm.date_created,
|
||||
)
|
||||
|
||||
with time_machine.travel(now, tick=False), mock.patch(
|
||||
"zilencer.views.send_android_push_notification", return_value=1
|
||||
), mock.patch("zilencer.views.send_apple_push_notification", return_value=1), mock.patch(
|
||||
"corporate.lib.stripe.RemoteServerBillingSession.current_count_for_billed_licenses",
|
||||
return_value=10,
|
||||
), self.assertLogs(
|
||||
"zilencer.views", level="INFO"
|
||||
):
|
||||
result = self.uuid_post(
|
||||
self.server_uuid,
|
||||
"/api/v1/remotes/push/notify",
|
||||
payload,
|
||||
content_type="application/json",
|
||||
subdomain="",
|
||||
)
|
||||
self.assert_json_success(result)
|
||||
|
||||
# The RemoteInstallationCount records get incremented again, and the RemoteRealmCount
|
||||
# gets collected.
|
||||
self.assertTableState(
|
||||
RemoteInstallationCount,
|
||||
["property", "value", "subgroup", "server", "remote_id", "end_time"],
|
||||
[
|
||||
[
|
||||
"mobile_pushes_received::day",
|
||||
9,
|
||||
None,
|
||||
self.server,
|
||||
None,
|
||||
ceiling_to_day(now),
|
||||
],
|
||||
[
|
||||
"mobile_pushes_forwarded::day",
|
||||
6,
|
||||
None,
|
||||
self.server,
|
||||
None,
|
||||
ceiling_to_day(now),
|
||||
],
|
||||
],
|
||||
)
|
||||
self.assertTableState(
|
||||
RemoteRealmCount,
|
||||
["property", "value", "subgroup", "server", "remote_realm", "remote_id", "end_time"],
|
||||
[
|
||||
[
|
||||
"mobile_pushes_received::day",
|
||||
3,
|
||||
None,
|
||||
self.server,
|
||||
remote_realm,
|
||||
None,
|
||||
ceiling_to_day(now),
|
||||
],
|
||||
[
|
||||
"mobile_pushes_forwarded::day",
|
||||
2,
|
||||
None,
|
||||
self.server,
|
||||
remote_realm,
|
||||
None,
|
||||
ceiling_to_day(now),
|
||||
],
|
||||
],
|
||||
)
|
||||
|
||||
def test_invites_sent(self) -> None:
|
||||
property = "invites_sent::day"
|
||||
|
||||
@@ -1783,12 +1507,12 @@ class TestDeleteStats(AnalyticsTestCase):
|
||||
FillState.objects.create(property="test", end_time=self.TIME_ZERO, state=FillState.DONE)
|
||||
|
||||
analytics = apps.get_app_config("analytics")
|
||||
for table in analytics.models.values():
|
||||
self.assertTrue(table._default_manager.exists())
|
||||
for table in list(analytics.models.values()):
|
||||
self.assertTrue(table.objects.exists())
|
||||
|
||||
do_drop_all_analytics_tables()
|
||||
for table in analytics.models.values():
|
||||
self.assertFalse(table._default_manager.exists())
|
||||
for table in list(analytics.models.values()):
|
||||
self.assertFalse(table.objects.exists())
|
||||
|
||||
def test_do_drop_single_stat(self) -> None:
|
||||
user = self.create_user()
|
||||
@@ -1807,17 +1531,16 @@ class TestDeleteStats(AnalyticsTestCase):
|
||||
FillState.objects.create(property="to_save", end_time=self.TIME_ZERO, state=FillState.DONE)
|
||||
|
||||
analytics = apps.get_app_config("analytics")
|
||||
for table in analytics.models.values():
|
||||
self.assertTrue(table._default_manager.exists())
|
||||
for table in list(analytics.models.values()):
|
||||
self.assertTrue(table.objects.exists())
|
||||
|
||||
do_drop_single_stat("to_delete")
|
||||
for table in analytics.models.values():
|
||||
self.assertFalse(table._default_manager.filter(property="to_delete").exists())
|
||||
self.assertTrue(table._default_manager.filter(property="to_save").exists())
|
||||
for table in list(analytics.models.values()):
|
||||
self.assertFalse(table.objects.filter(property="to_delete").exists())
|
||||
self.assertTrue(table.objects.filter(property="to_save").exists())
|
||||
|
||||
|
||||
class TestActiveUsersAudit(AnalyticsTestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.user = self.create_user()
|
||||
@@ -2000,7 +1723,6 @@ class TestActiveUsersAudit(AnalyticsTestCase):
|
||||
|
||||
|
||||
class TestRealmActiveHumans(AnalyticsTestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.stat = COUNT_STATS["realm_active_humans::day"]
|
||||
@@ -2120,26 +1842,3 @@ class TestRealmActiveHumans(AnalyticsTestCase):
|
||||
1,
|
||||
)
|
||||
self.assertEqual(RealmCount.objects.filter(property="realm_active_humans::day").count(), 1)
|
||||
|
||||
|
||||
class GetLastIdFromServerTest(ZulipTestCase):
|
||||
def test_get_last_id_from_server_ignores_null(self) -> None:
|
||||
"""
|
||||
Verifies that get_last_id_from_server ignores null remote_ids, since this goes
|
||||
against the default Postgres ordering behavior, which treats nulls as the largest value.
|
||||
"""
|
||||
self.server_uuid = "6cde5f7a-1f7e-4978-9716-49f69ebfc9fe"
|
||||
self.server = RemoteZulipServer.objects.create(
|
||||
uuid=self.server_uuid,
|
||||
api_key="magic_secret_api_key",
|
||||
hostname="demo.example.com",
|
||||
last_updated=timezone_now(),
|
||||
)
|
||||
first = RemoteInstallationCount.objects.create(
|
||||
end_time=timezone_now(), server=self.server, property="test", value=1, remote_id=1
|
||||
)
|
||||
RemoteInstallationCount.objects.create(
|
||||
end_time=timezone_now(), server=self.server, property="test2", value=1, remote_id=None
|
||||
)
|
||||
result = get_last_id_from_server(self.server, RemoteInstallationCount)
|
||||
self.assertEqual(result, first.remote_id)
|
||||
|
||||
@@ -2,16 +2,14 @@ from datetime import datetime, timedelta, timezone
|
||||
from typing import List, Optional
|
||||
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import override
|
||||
|
||||
from analytics.lib.counts import COUNT_STATS, CountStat
|
||||
from analytics.lib.time_utils import time_range
|
||||
from analytics.models import FillState, RealmCount, StreamCount, UserCount
|
||||
from analytics.models import FillState, RealmCount, UserCount
|
||||
from analytics.views.stats import rewrite_client_arrays, sort_by_totals, sort_client_labels
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.timestamp import ceiling_to_day, ceiling_to_hour, datetime_to_timestamp
|
||||
from zerver.models import Client
|
||||
from zerver.models.realms import get_realm
|
||||
from zerver.models import Client, get_realm
|
||||
|
||||
|
||||
class TestStatsEndpoint(ZulipTestCase):
|
||||
@@ -70,12 +68,10 @@ class TestStatsEndpoint(ZulipTestCase):
|
||||
|
||||
|
||||
class TestGetChartData(ZulipTestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.realm = get_realm("zulip")
|
||||
self.user = self.example_user("hamlet")
|
||||
self.stream_id = self.get_stream_id(self.get_streams(self.user)[0])
|
||||
self.login_user(self.user)
|
||||
self.end_times_hour = [
|
||||
ceiling_to_hour(self.realm.date_created) + timedelta(hours=i) for i in range(4)
|
||||
@@ -118,17 +114,6 @@ class TestGetChartData(ZulipTestCase):
|
||||
)
|
||||
for i, subgroup in enumerate(user_subgroups)
|
||||
)
|
||||
StreamCount.objects.bulk_create(
|
||||
StreamCount(
|
||||
property=stat.property,
|
||||
subgroup=subgroup,
|
||||
end_time=insert_time,
|
||||
value=100 + i,
|
||||
stream_id=self.stream_id,
|
||||
realm=self.realm,
|
||||
)
|
||||
for i, subgroup in enumerate(realm_subgroups)
|
||||
)
|
||||
FillState.objects.create(property=stat.property, end_time=fill_time, state=FillState.DONE)
|
||||
|
||||
def test_number_of_humans(self) -> None:
|
||||
@@ -265,49 +250,6 @@ class TestGetChartData(ZulipTestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_messages_sent_by_stream(self) -> None:
|
||||
stat = COUNT_STATS["messages_in_stream:is_bot:day"]
|
||||
self.insert_data(stat, ["true", "false"], [])
|
||||
|
||||
result = self.client_get(
|
||||
f"/json/analytics/chart_data/stream/{self.stream_id}",
|
||||
{
|
||||
"chart_name": "messages_sent_by_stream",
|
||||
},
|
||||
)
|
||||
data = self.assert_json_success(result)
|
||||
self.assertEqual(
|
||||
data,
|
||||
{
|
||||
"msg": "",
|
||||
"end_times": [datetime_to_timestamp(dt) for dt in self.end_times_day],
|
||||
"frequency": CountStat.DAY,
|
||||
"everyone": {"bot": self.data(100), "human": self.data(101)},
|
||||
"display_order": None,
|
||||
"result": "success",
|
||||
},
|
||||
)
|
||||
|
||||
result = self.api_get(
|
||||
self.example_user("polonius"),
|
||||
f"/api/v1/analytics/chart_data/stream/{self.stream_id}",
|
||||
{
|
||||
"chart_name": "messages_sent_by_stream",
|
||||
},
|
||||
)
|
||||
self.assert_json_error(result, "Not allowed for guest users")
|
||||
|
||||
# Verify we correctly forbid access to stats of streams in other realms.
|
||||
result = self.api_get(
|
||||
self.mit_user("sipbtest"),
|
||||
f"/api/v1/analytics/chart_data/stream/{self.stream_id}",
|
||||
{
|
||||
"chart_name": "messages_sent_by_stream",
|
||||
},
|
||||
subdomain="zephyr",
|
||||
)
|
||||
self.assert_json_error(result, "Invalid stream ID")
|
||||
|
||||
def test_include_empty_subgroups(self) -> None:
|
||||
FillState.objects.create(
|
||||
property="realm_active_humans::day",
|
||||
|
||||
@@ -1,173 +1,30 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from unittest import mock
|
||||
|
||||
import orjson
|
||||
import time_machine
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import override
|
||||
|
||||
from corporate.lib.stripe import RealmBillingSession, add_months
|
||||
from corporate.models import (
|
||||
Customer,
|
||||
CustomerPlan,
|
||||
LicenseLedger,
|
||||
SponsoredPlanTypes,
|
||||
ZulipSponsorshipRequest,
|
||||
get_current_plan_by_realm,
|
||||
get_customer_by_realm,
|
||||
)
|
||||
from corporate.lib.stripe import add_months, update_sponsorship_status
|
||||
from corporate.models import Customer, CustomerPlan, LicenseLedger, get_customer_by_realm
|
||||
from zerver.actions.invites import do_create_multiuse_invite_link
|
||||
from zerver.actions.realm_settings import do_change_realm_org_type, do_send_realm_reactivation_email
|
||||
from zerver.actions.user_settings import do_change_user_setting
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.test_helpers import reset_email_visibility_to_everyone_in_zulip_realm
|
||||
from zerver.models import MultiuseInvite, PreregistrationUser, Realm, UserMessage, UserProfile
|
||||
from zerver.models.realms import OrgTypeEnum, get_org_type_display_name, get_realm
|
||||
from zilencer.lib.remote_counts import MissingDataError
|
||||
from zerver.models import (
|
||||
MultiuseInvite,
|
||||
PreregistrationUser,
|
||||
Realm,
|
||||
UserMessage,
|
||||
UserProfile,
|
||||
get_org_type_display_name,
|
||||
get_realm,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
|
||||
|
||||
import uuid
|
||||
|
||||
from zilencer.models import RemoteZulipServer
|
||||
|
||||
|
||||
class TestRemoteServerSupportEndpoint(ZulipTestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
def add_sponsorship_request(
|
||||
hostname: str, org_type: int, website: str, paid_users: str, plan: str
|
||||
) -> None:
|
||||
remote_server = RemoteZulipServer.objects.get(hostname=hostname)
|
||||
customer = Customer.objects.create(
|
||||
remote_server=remote_server, sponsorship_pending=True
|
||||
)
|
||||
ZulipSponsorshipRequest.objects.create(
|
||||
customer=customer,
|
||||
org_type=org_type,
|
||||
org_website=website,
|
||||
org_description="We help people.",
|
||||
expected_total_users="20-35",
|
||||
paid_users_count=paid_users,
|
||||
paid_users_description="",
|
||||
requested_plan=plan,
|
||||
)
|
||||
|
||||
super().setUp()
|
||||
|
||||
# Set up some initial example data.
|
||||
for i in range(20):
|
||||
hostname = f"zulip-{i}.example.com"
|
||||
RemoteZulipServer.objects.create(
|
||||
hostname=hostname, contact_email=f"admin@{hostname}", plan_type=1, uuid=uuid.uuid4()
|
||||
)
|
||||
|
||||
# Add example sponsorship request data
|
||||
add_sponsorship_request(
|
||||
hostname="zulip-1.example.com",
|
||||
org_type=OrgTypeEnum.Community.value,
|
||||
website="",
|
||||
paid_users="None",
|
||||
plan=SponsoredPlanTypes.BUSINESS.value,
|
||||
)
|
||||
|
||||
add_sponsorship_request(
|
||||
hostname="zulip-2.example.com",
|
||||
org_type=OrgTypeEnum.OpenSource.value,
|
||||
website="example.org",
|
||||
paid_users="",
|
||||
plan=SponsoredPlanTypes.COMMUNITY.value,
|
||||
)
|
||||
|
||||
def test_search(self) -> None:
|
||||
self.login("cordelia")
|
||||
|
||||
result = self.client_get("/activity/remote/support")
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(result["Location"], "/login/")
|
||||
|
||||
# Iago is the user with the appropriate permissions to access this page.
|
||||
self.login("iago")
|
||||
assert self.example_user("iago").is_staff
|
||||
|
||||
result = self.client_get("/activity/remote/support")
|
||||
self.assert_in_success_response(
|
||||
[
|
||||
'input type="text" name="q" class="input-xxlarge search-query" placeholder="hostname or contact email"'
|
||||
],
|
||||
result,
|
||||
)
|
||||
|
||||
with mock.patch("analytics.views.support.compute_max_monthly_messages", return_value=1000):
|
||||
result = self.client_get("/activity/remote/support", {"q": "zulip-1.example.com"})
|
||||
self.assert_in_success_response(["<h3>zulip-1.example.com</h3>"], result)
|
||||
self.assert_in_success_response(["<b>Max monthly messages</b>: 1000"], result)
|
||||
self.assert_not_in_success_response(["<h3>zulip-2.example.com</h3>"], result)
|
||||
|
||||
# Sponsorship request information
|
||||
self.assert_in_success_response(["<li><b>Organization type</b>: Community</li>"], result)
|
||||
self.assert_in_success_response(
|
||||
["<li><b>Organization website</b>: No website submitted</li>"], result
|
||||
)
|
||||
self.assert_in_success_response(["<li><b>Paid users</b>: None</li>"], result)
|
||||
self.assert_in_success_response(["<li><b>Requested plan</b>: Business</li>"], result)
|
||||
self.assert_in_success_response(
|
||||
["<li><b>Organization description</b>: We help people.</li>"], result
|
||||
)
|
||||
self.assert_in_success_response(["<li><b>Estimated total users</b>: 20-35</li>"], result)
|
||||
self.assert_in_success_response(["<li><b>Description of paid users</b>: </li>"], result)
|
||||
|
||||
with mock.patch(
|
||||
"analytics.views.support.compute_max_monthly_messages", side_effect=MissingDataError
|
||||
):
|
||||
result = self.client_get("/activity/remote/support", {"q": "zulip-1.example.com"})
|
||||
self.assert_in_success_response(["<h3>zulip-1.example.com</h3>"], result)
|
||||
self.assert_in_success_response(
|
||||
["<b>Max monthly messages</b>: Recent data missing"], result
|
||||
)
|
||||
self.assert_not_in_success_response(["<h3>zulip-2.example.com</h3>"], result)
|
||||
|
||||
result = self.client_get("/activity/remote/support", {"q": "example.com"})
|
||||
for i in range(20):
|
||||
self.assert_in_success_response([f"<h3>zulip-{i}.example.com</h3>"], result)
|
||||
|
||||
result = self.client_get("/activity/remote/support", {"q": "admin@zulip-2.example.com"})
|
||||
self.assert_in_success_response(["<h3>zulip-2.example.com</h3>"], result)
|
||||
self.assert_in_success_response(["<b>Contact email</b>: admin@zulip-2.example.com"], result)
|
||||
self.assert_not_in_success_response(["<h3>zulip-1.example.com</h3>"], result)
|
||||
|
||||
# Sponsorship request information
|
||||
self.assert_in_success_response(
|
||||
["<li><b>Organization type</b>: Open-source project</li>"], result
|
||||
)
|
||||
self.assert_in_success_response(
|
||||
["<li><b>Organization website</b>: example.org</li>"], result
|
||||
)
|
||||
self.assert_in_success_response(["<li><b>Paid users</b>: </li>"], result)
|
||||
self.assert_in_success_response(["<li><b>Requested plan</b>: Community</li>"], result)
|
||||
self.assert_in_success_response(
|
||||
["<li><b>Organization description</b>: We help people.</li>"], result
|
||||
)
|
||||
self.assert_in_success_response(["<li><b>Estimated total users</b>: 20-35</li>"], result)
|
||||
self.assert_in_success_response(["<li><b>Description of paid users</b>: </li>"], result)
|
||||
|
||||
result = self.client_get("/activity/remote/support", {"q": "admin@zulip-3.example.com"})
|
||||
self.assert_in_success_response(["<h3>zulip-3.example.com</h3>"], result)
|
||||
self.assert_in_success_response(["<b>Contact email</b>: admin@zulip-3.example.com"], result)
|
||||
self.assert_not_in_success_response(["<h3>zulip-1.example.com</h3>"], result)
|
||||
|
||||
# Sponsorship request information
|
||||
self.assert_not_in_success_response(
|
||||
["<li><b>Organization description</b>: We help people.</li>"], result
|
||||
)
|
||||
self.assert_not_in_success_response(
|
||||
["<li><b>Estimated total users</b>: 20-35</li>"], result
|
||||
)
|
||||
self.assert_not_in_success_response(["<li><b>Description of paid users</b>: </li>"], result)
|
||||
|
||||
|
||||
class TestSupportEndpoint(ZulipTestCase):
|
||||
def test_search(self) -> None:
|
||||
@@ -270,7 +127,7 @@ class TestSupportEndpoint(ZulipTestCase):
|
||||
'<option value="deactivated" >Deactivated</option>',
|
||||
'scrub-realm-button">',
|
||||
'data-string-id="lear"',
|
||||
"<b>Plan name</b>: Zulip Cloud Standard",
|
||||
"<b>Name</b>: Zulip Cloud Standard",
|
||||
"<b>Status</b>: Active",
|
||||
"<b>Billing schedule</b>: Annual",
|
||||
"<b>Licenses</b>: 2/10 (Manual)",
|
||||
@@ -370,8 +227,8 @@ class TestSupportEndpoint(ZulipTestCase):
|
||||
plan = CustomerPlan.objects.create(
|
||||
customer=customer,
|
||||
billing_cycle_anchor=now,
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL,
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
billing_schedule=CustomerPlan.ANNUAL,
|
||||
tier=CustomerPlan.STANDARD,
|
||||
price_per_license=8000,
|
||||
next_invoice_date=add_months(now, 12),
|
||||
)
|
||||
@@ -435,56 +292,55 @@ class TestSupportEndpoint(ZulipTestCase):
|
||||
check_zulip_realm_query_result(result)
|
||||
check_lear_realm_query_result(result)
|
||||
|
||||
self.client_post("/accounts/home/", {"email": self.nonreg_email("test")})
|
||||
self.login("iago")
|
||||
with mock.patch(
|
||||
"analytics.views.support.timezone_now",
|
||||
return_value=timezone_now() - timedelta(minutes=50),
|
||||
):
|
||||
self.client_post("/accounts/home/", {"email": self.nonreg_email("test")})
|
||||
self.login("iago")
|
||||
result = get_check_query_result(self.nonreg_email("test"), 1)
|
||||
check_preregistration_user_query_result(result, self.nonreg_email("test"))
|
||||
check_zulip_realm_query_result(result)
|
||||
|
||||
def query_result_from_before(*args: Any) -> "TestHttpResponse":
|
||||
with time_machine.travel((timezone_now() - timedelta(minutes=50)), tick=False):
|
||||
return get_check_query_result(*args)
|
||||
create_invitation("Denmark", self.nonreg_email("test1"))
|
||||
result = get_check_query_result(self.nonreg_email("test1"), 1)
|
||||
check_preregistration_user_query_result(result, self.nonreg_email("test1"), invite=True)
|
||||
check_zulip_realm_query_result(result)
|
||||
|
||||
result = query_result_from_before(self.nonreg_email("test"), 1)
|
||||
check_preregistration_user_query_result(result, self.nonreg_email("test"))
|
||||
check_zulip_realm_query_result(result)
|
||||
email = self.nonreg_email("alice")
|
||||
self.submit_realm_creation_form(
|
||||
email, realm_subdomain="zuliptest", realm_name="Zulip test"
|
||||
)
|
||||
result = get_check_query_result(email, 1)
|
||||
check_realm_creation_query_result(result, email)
|
||||
|
||||
create_invitation("Denmark", self.nonreg_email("test1"))
|
||||
result = query_result_from_before(self.nonreg_email("test1"), 1)
|
||||
check_preregistration_user_query_result(result, self.nonreg_email("test1"), invite=True)
|
||||
check_zulip_realm_query_result(result)
|
||||
invite_expires_in_minutes = 10 * 24 * 60
|
||||
do_create_multiuse_invite_link(
|
||||
self.example_user("hamlet"),
|
||||
invited_as=1,
|
||||
invite_expires_in_minutes=invite_expires_in_minutes,
|
||||
)
|
||||
result = get_check_query_result("zulip", 2)
|
||||
check_multiuse_invite_link_query_result(result)
|
||||
check_zulip_realm_query_result(result)
|
||||
MultiuseInvite.objects.all().delete()
|
||||
|
||||
email = self.nonreg_email("alice")
|
||||
self.submit_realm_creation_form(
|
||||
email, realm_subdomain="custom-test", realm_name="Zulip test"
|
||||
)
|
||||
result = query_result_from_before(email, 1)
|
||||
check_realm_creation_query_result(result, email)
|
||||
do_send_realm_reactivation_email(get_realm("zulip"), acting_user=None)
|
||||
result = get_check_query_result("zulip", 2)
|
||||
check_realm_reactivation_link_query_result(result)
|
||||
check_zulip_realm_query_result(result)
|
||||
|
||||
invite_expires_in_minutes = 10 * 24 * 60
|
||||
do_create_multiuse_invite_link(
|
||||
self.example_user("hamlet"),
|
||||
invited_as=1,
|
||||
invite_expires_in_minutes=invite_expires_in_minutes,
|
||||
)
|
||||
result = query_result_from_before("zulip", 2)
|
||||
check_multiuse_invite_link_query_result(result)
|
||||
check_zulip_realm_query_result(result)
|
||||
MultiuseInvite.objects.all().delete()
|
||||
lear_nonreg_email = "newguy@lear.org"
|
||||
self.client_post("/accounts/home/", {"email": lear_nonreg_email}, subdomain="lear")
|
||||
result = get_check_query_result(lear_nonreg_email, 1)
|
||||
check_preregistration_user_query_result(result, lear_nonreg_email)
|
||||
check_lear_realm_query_result(result)
|
||||
|
||||
do_send_realm_reactivation_email(get_realm("zulip"), acting_user=None)
|
||||
result = query_result_from_before("zulip", 2)
|
||||
check_realm_reactivation_link_query_result(result)
|
||||
check_zulip_realm_query_result(result)
|
||||
|
||||
lear_nonreg_email = "newguy@lear.org"
|
||||
self.client_post("/accounts/home/", {"email": lear_nonreg_email}, subdomain="lear")
|
||||
result = query_result_from_before(lear_nonreg_email, 1)
|
||||
check_preregistration_user_query_result(result, lear_nonreg_email)
|
||||
check_lear_realm_query_result(result)
|
||||
|
||||
self.login_user(lear_user)
|
||||
create_invitation("general", "newguy2@lear.org", lear_realm)
|
||||
result = query_result_from_before("newguy2@lear.org", 1, lear_realm.string_id)
|
||||
check_preregistration_user_query_result(result, "newguy2@lear.org", invite=True)
|
||||
check_lear_realm_query_result(result)
|
||||
self.login_user(lear_user)
|
||||
create_invitation("general", "newguy2@lear.org", lear_realm)
|
||||
result = get_check_query_result("newguy2@lear.org", 1, lear_realm.string_id)
|
||||
check_preregistration_user_query_result(result, "newguy2@lear.org", invite=True)
|
||||
check_lear_realm_query_result(result)
|
||||
|
||||
def test_get_org_type_display_name(self) -> None:
|
||||
self.assertEqual(get_org_type_display_name(Realm.ORG_TYPES["business"]["id"]), "Business")
|
||||
@@ -512,50 +368,38 @@ class TestSupportEndpoint(ZulipTestCase):
|
||||
result,
|
||||
)
|
||||
|
||||
def test_change_billing_modality(self) -> None:
|
||||
realm = get_realm("zulip")
|
||||
@mock.patch("analytics.views.support.update_billing_method_of_current_plan")
|
||||
def test_change_billing_method(self, m: mock.Mock) -> None:
|
||||
cordelia = self.example_user("cordelia")
|
||||
self.login_user(cordelia)
|
||||
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{"realm_id": f"{realm.id}", "billing_method": "charge_automatically"},
|
||||
"/activity/support", {"realm_id": f"{cordelia.realm_id}", "plan_type": "2"}
|
||||
)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(result["Location"], "/login/")
|
||||
|
||||
customer = Customer.objects.create(realm=realm, stripe_customer_id="cus_12345")
|
||||
CustomerPlan.objects.create(
|
||||
customer=customer,
|
||||
status=CustomerPlan.ACTIVE,
|
||||
billing_cycle_anchor=timezone_now(),
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL,
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
)
|
||||
|
||||
iago = self.example_user("iago")
|
||||
self.login_user(iago)
|
||||
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{"realm_id": f"{realm.id}", "billing_modality": "charge_automatically"},
|
||||
{"realm_id": f"{iago.realm_id}", "billing_method": "charge_automatically"},
|
||||
)
|
||||
m.assert_called_once_with(get_realm("zulip"), charge_automatically=True, acting_user=iago)
|
||||
self.assert_in_success_response(
|
||||
["Billing collection method of zulip updated to charge automatically"], result
|
||||
["Billing method of zulip updated to charge automatically"], result
|
||||
)
|
||||
plan = get_current_plan_by_realm(realm)
|
||||
assert plan is not None
|
||||
self.assertEqual(plan.charge_automatically, True)
|
||||
|
||||
m.reset_mock()
|
||||
|
||||
result = self.client_post(
|
||||
"/activity/support", {"realm_id": f"{realm.id}", "billing_modality": "send_invoice"}
|
||||
"/activity/support", {"realm_id": f"{iago.realm_id}", "billing_method": "send_invoice"}
|
||||
)
|
||||
m.assert_called_once_with(get_realm("zulip"), charge_automatically=False, acting_user=iago)
|
||||
self.assert_in_success_response(
|
||||
["Billing collection method of zulip updated to send invoice"], result
|
||||
["Billing method of zulip updated to pay by invoice"], result
|
||||
)
|
||||
realm.refresh_from_db()
|
||||
plan = get_current_plan_by_realm(realm)
|
||||
assert plan is not None
|
||||
self.assertEqual(plan.charge_automatically, False)
|
||||
|
||||
def test_change_realm_plan_type(self) -> None:
|
||||
cordelia = self.example_user("cordelia")
|
||||
@@ -576,7 +420,7 @@ class TestSupportEndpoint(ZulipTestCase):
|
||||
)
|
||||
m.assert_called_once_with(get_realm("zulip"), 2, acting_user=iago)
|
||||
self.assert_in_success_response(
|
||||
["Plan type of zulip changed from Self-hosted to Limited"], result
|
||||
["Plan type of zulip changed from self-hosted to limited"], result
|
||||
)
|
||||
|
||||
with mock.patch("analytics.views.support.do_change_realm_plan_type") as m:
|
||||
@@ -585,7 +429,7 @@ class TestSupportEndpoint(ZulipTestCase):
|
||||
)
|
||||
m.assert_called_once_with(get_realm("zulip"), 10, acting_user=iago)
|
||||
self.assert_in_success_response(
|
||||
["Plan type of zulip changed from Self-hosted to Plus"], result
|
||||
["Plan type of zulip changed from self-hosted to plus"], result
|
||||
)
|
||||
|
||||
def test_change_org_type(self) -> None:
|
||||
@@ -622,15 +466,14 @@ class TestSupportEndpoint(ZulipTestCase):
|
||||
self.assertEqual(result["Location"], "/login/")
|
||||
|
||||
iago = self.example_user("iago")
|
||||
self.login_user(iago)
|
||||
self.login("iago")
|
||||
|
||||
result = self.client_post(
|
||||
"/activity/support", {"realm_id": f"{lear_realm.id}", "discount": "25"}
|
||||
)
|
||||
self.assert_in_success_response(["Discount for lear changed to 25% from 0%"], result)
|
||||
customer = get_customer_by_realm(lear_realm)
|
||||
assert customer is not None
|
||||
self.assertEqual(customer.default_discount, Decimal(25))
|
||||
with mock.patch("analytics.views.support.attach_discount_to_realm") as m:
|
||||
result = self.client_post(
|
||||
"/activity/support", {"realm_id": f"{lear_realm.id}", "discount": "25"}
|
||||
)
|
||||
m.assert_called_once_with(get_realm("lear"), 25, acting_user=iago)
|
||||
self.assert_in_success_response(["Discount of lear changed to 25% from 0%"], result)
|
||||
|
||||
def test_change_sponsorship_status(self) -> None:
|
||||
lear_realm = get_realm("lear")
|
||||
@@ -665,12 +508,8 @@ class TestSupportEndpoint(ZulipTestCase):
|
||||
self.assertFalse(customer.sponsorship_pending)
|
||||
|
||||
def test_approve_sponsorship(self) -> None:
|
||||
support_admin = self.example_user("iago")
|
||||
lear_realm = get_realm("lear")
|
||||
billing_session = RealmBillingSession(
|
||||
user=support_admin, realm=lear_realm, support_session=True
|
||||
)
|
||||
billing_session.update_customer_sponsorship_status(True)
|
||||
update_sponsorship_status(lear_realm, True, acting_user=None)
|
||||
king_user = self.lear_user("king")
|
||||
king_user.role = UserProfile.ROLE_REALM_OWNER
|
||||
king_user.save()
|
||||
@@ -785,84 +624,73 @@ class TestSupportEndpoint(ZulipTestCase):
|
||||
["Subdomain reserved. Please choose a different one."], result
|
||||
)
|
||||
|
||||
def test_modify_plan_for_downgrade_at_end_of_billing_cycle(self) -> None:
|
||||
realm = get_realm("zulip")
|
||||
def test_downgrade_realm(self) -> None:
|
||||
cordelia = self.example_user("cordelia")
|
||||
self.login_user(cordelia)
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{"realm_id": f"{realm.id}", "modify_plan": "downgrade_at_billing_cycle_end"},
|
||||
"/activity/support", {"realm_id": f"{cordelia.realm_id}", "plan_type": "2"}
|
||||
)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(result["Location"], "/login/")
|
||||
|
||||
customer = Customer.objects.create(realm=realm, stripe_customer_id="cus_12345")
|
||||
CustomerPlan.objects.create(
|
||||
customer=customer,
|
||||
status=CustomerPlan.ACTIVE,
|
||||
billing_cycle_anchor=timezone_now(),
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL,
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
)
|
||||
|
||||
iago = self.example_user("iago")
|
||||
self.login_user(iago)
|
||||
|
||||
with self.assertLogs("corporate.stripe", "INFO") as m:
|
||||
with mock.patch("analytics.views.support.downgrade_at_the_end_of_billing_cycle") as m:
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{
|
||||
"realm_id": f"{realm.id}",
|
||||
"realm_id": f"{iago.realm_id}",
|
||||
"modify_plan": "downgrade_at_billing_cycle_end",
|
||||
},
|
||||
)
|
||||
m.assert_called_once_with(get_realm("zulip"))
|
||||
self.assert_in_success_response(
|
||||
["zulip marked for downgrade at the end of billing cycle"], result
|
||||
)
|
||||
plan = get_current_plan_by_realm(realm)
|
||||
assert plan is not None
|
||||
self.assertEqual(plan.status, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE)
|
||||
expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {customer.id}, CustomerPlan.id: {plan.id}, status: {CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}"
|
||||
self.assertEqual(m.output[0], expected_log)
|
||||
|
||||
def test_modify_plan_for_downgrade_now_without_additional_licenses(self) -> None:
|
||||
realm = get_realm("zulip")
|
||||
cordelia = self.example_user("cordelia")
|
||||
self.login_user(cordelia)
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{"realm_id": f"{realm.id}", "modify_plan": "downgrade_now_without_additional_licenses"},
|
||||
)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(result["Location"], "/login/")
|
||||
with mock.patch(
|
||||
"analytics.views.support.downgrade_now_without_creating_additional_invoices"
|
||||
) as m:
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{
|
||||
"realm_id": f"{iago.realm_id}",
|
||||
"modify_plan": "downgrade_now_without_additional_licenses",
|
||||
},
|
||||
)
|
||||
m.assert_called_once_with(get_realm("zulip"))
|
||||
self.assert_in_success_response(
|
||||
["zulip downgraded without creating additional invoices"], result
|
||||
)
|
||||
|
||||
customer = Customer.objects.create(realm=realm, stripe_customer_id="cus_12345")
|
||||
plan = CustomerPlan.objects.create(
|
||||
customer=customer,
|
||||
status=CustomerPlan.ACTIVE,
|
||||
billing_cycle_anchor=timezone_now(),
|
||||
billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL,
|
||||
tier=CustomerPlan.TIER_CLOUD_STANDARD,
|
||||
)
|
||||
with mock.patch(
|
||||
"analytics.views.support.downgrade_now_without_creating_additional_invoices"
|
||||
) as m1:
|
||||
with mock.patch("analytics.views.support.void_all_open_invoices", return_value=1) as m2:
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{
|
||||
"realm_id": f"{iago.realm_id}",
|
||||
"modify_plan": "downgrade_now_void_open_invoices",
|
||||
},
|
||||
)
|
||||
m1.assert_called_once_with(get_realm("zulip"))
|
||||
m2.assert_called_once_with(get_realm("zulip"))
|
||||
self.assert_in_success_response(
|
||||
["zulip downgraded and voided 1 open invoices"], result
|
||||
)
|
||||
|
||||
iago = self.example_user("iago")
|
||||
self.login_user(iago)
|
||||
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{
|
||||
"realm_id": f"{iago.realm_id}",
|
||||
"modify_plan": "downgrade_now_without_additional_licenses",
|
||||
},
|
||||
)
|
||||
self.assert_in_success_response(
|
||||
["zulip downgraded without creating additional invoices"], result
|
||||
)
|
||||
|
||||
plan.refresh_from_db()
|
||||
self.assertEqual(plan.status, CustomerPlan.ENDED)
|
||||
realm.refresh_from_db()
|
||||
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_LIMITED)
|
||||
with mock.patch("analytics.views.support.switch_realm_from_standard_to_plus_plan") as m:
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{
|
||||
"realm_id": f"{iago.realm_id}",
|
||||
"modify_plan": "upgrade_to_plus",
|
||||
},
|
||||
)
|
||||
m.assert_called_once_with(get_realm("zulip"))
|
||||
self.assert_in_success_response(["zulip upgraded to Plus"], result)
|
||||
|
||||
def test_scrub_realm(self) -> None:
|
||||
cordelia = self.example_user("cordelia")
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
from typing import List, Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls.resolvers import URLPattern, URLResolver
|
||||
|
||||
from analytics.views.installation_activity import (
|
||||
get_installation_activity,
|
||||
get_integrations_activity,
|
||||
)
|
||||
from analytics.views.installation_activity import get_installation_activity
|
||||
from analytics.views.realm_activity import get_realm_activity
|
||||
from analytics.views.stats import (
|
||||
get_chart_data,
|
||||
get_chart_data_for_installation,
|
||||
get_chart_data_for_realm,
|
||||
get_chart_data_for_stream,
|
||||
get_chart_data_for_remote_installation,
|
||||
get_chart_data_for_remote_realm,
|
||||
stats,
|
||||
stats_for_installation,
|
||||
stats_for_realm,
|
||||
stats_for_remote_installation,
|
||||
stats_for_remote_realm,
|
||||
)
|
||||
from analytics.views.support import support
|
||||
from analytics.views.user_activity import get_user_activity
|
||||
@@ -26,31 +25,19 @@ from zerver.lib.rest import rest_path
|
||||
i18n_urlpatterns: List[Union[URLPattern, URLResolver]] = [
|
||||
# Server admin (user_profile.is_staff) visible stats pages
|
||||
path("activity", get_installation_activity),
|
||||
path("activity/integrations", get_integrations_activity),
|
||||
path("activity/support", support, name="support"),
|
||||
path("realm_activity/<realm_str>/", get_realm_activity),
|
||||
path("user_activity/<user_profile_id>/", get_user_activity),
|
||||
path("stats/realm/<realm_str>/", stats_for_realm),
|
||||
path("stats/installation", stats_for_installation),
|
||||
path("stats/remote/<int:remote_server_id>/installation", stats_for_remote_installation),
|
||||
path(
|
||||
"stats/remote/<int:remote_server_id>/realm/<int:remote_realm_id>/", stats_for_remote_realm
|
||||
),
|
||||
# User-visible stats page
|
||||
path("stats", stats, name="stats"),
|
||||
]
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
from analytics.views.remote_activity import get_remote_server_activity
|
||||
from analytics.views.stats import stats_for_remote_installation, stats_for_remote_realm
|
||||
from analytics.views.support import remote_servers_support
|
||||
|
||||
i18n_urlpatterns += [
|
||||
path("activity/remote", get_remote_server_activity),
|
||||
path("stats/remote/<int:remote_server_id>/installation", stats_for_remote_installation),
|
||||
path(
|
||||
"stats/remote/<int:remote_server_id>/realm/<int:remote_realm_id>/",
|
||||
stats_for_remote_realm,
|
||||
),
|
||||
path("activity/remote/support", remote_servers_support, name="remote_servers_support"),
|
||||
]
|
||||
|
||||
# These endpoints are a part of the API (V1), which uses:
|
||||
# * REST verbs
|
||||
# * Basic auth (username:password is email:apiKey)
|
||||
@@ -62,28 +49,18 @@ if settings.ZILENCER_ENABLED:
|
||||
v1_api_and_json_patterns = [
|
||||
# get data for the graphs at /stats
|
||||
rest_path("analytics/chart_data", GET=get_chart_data),
|
||||
rest_path("analytics/chart_data/stream/<stream_id>", GET=get_chart_data_for_stream),
|
||||
rest_path("analytics/chart_data/realm/<realm_str>", GET=get_chart_data_for_realm),
|
||||
rest_path("analytics/chart_data/installation", GET=get_chart_data_for_installation),
|
||||
rest_path(
|
||||
"analytics/chart_data/remote/<int:remote_server_id>/installation",
|
||||
GET=get_chart_data_for_remote_installation,
|
||||
),
|
||||
rest_path(
|
||||
"analytics/chart_data/remote/<int:remote_server_id>/realm/<int:remote_realm_id>",
|
||||
GET=get_chart_data_for_remote_realm,
|
||||
),
|
||||
]
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
from analytics.views.stats import (
|
||||
get_chart_data_for_remote_installation,
|
||||
get_chart_data_for_remote_realm,
|
||||
)
|
||||
|
||||
v1_api_and_json_patterns += [
|
||||
rest_path(
|
||||
"analytics/chart_data/remote/<int:remote_server_id>/installation",
|
||||
GET=get_chart_data_for_remote_installation,
|
||||
),
|
||||
rest_path(
|
||||
"analytics/chart_data/remote/<int:remote_server_id>/realm/<int:remote_realm_id>",
|
||||
GET=get_chart_data_for_remote_realm,
|
||||
),
|
||||
]
|
||||
|
||||
i18n_urlpatterns += [
|
||||
path("api/v1/", include(v1_api_and_json_patterns)),
|
||||
path("json/", include(v1_api_and_json_patterns)),
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Collection, Dict, List, Optional, Sequence, Union
|
||||
from typing import Any, Collection, Dict, List, Optional, Sequence
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.db.backends.utils import CursorWrapper
|
||||
from django.template import loader
|
||||
from django.urls import reverse
|
||||
from markupsafe import Markup
|
||||
from psycopg2.sql import Composable
|
||||
|
||||
from zerver.lib.pysa import mark_sanitized
|
||||
from zerver.lib.url_encoding import append_url_query_string
|
||||
from zerver.models import Realm, UserActivity
|
||||
from zerver.models import UserActivity, get_realm
|
||||
|
||||
if sys.version_info < (3, 9): # nocoverage
|
||||
from backports import zoneinfo
|
||||
@@ -48,24 +45,6 @@ def make_table(
|
||||
return content
|
||||
|
||||
|
||||
def fix_rows(
|
||||
rows: List[List[Any]],
|
||||
i: int,
|
||||
fixup_func: Union[Callable[[str], Markup], Callable[[datetime], str]],
|
||||
) -> None:
|
||||
for row in rows:
|
||||
row[i] = fixup_func(row[i])
|
||||
|
||||
|
||||
def get_query_data(query: Composable) -> List[List[Any]]:
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(query)
|
||||
rows = cursor.fetchall()
|
||||
rows = list(map(list, rows))
|
||||
cursor.close()
|
||||
return rows
|
||||
|
||||
|
||||
def dictfetchall(cursor: CursorWrapper) -> List[Dict[str, Any]]:
|
||||
"""Returns all rows from a cursor as a dict"""
|
||||
desc = cursor.description
|
||||
@@ -104,27 +83,21 @@ def realm_support_link(realm_str: str) -> Markup:
|
||||
support_url = reverse("support")
|
||||
query = urlencode({"q": realm_str})
|
||||
url = append_url_query_string(support_url, query)
|
||||
return Markup('<a href="{url}"><i class="fa fa-gear"></i></a>').format(url=url)
|
||||
return Markup('<a href="{url}">{realm_str}</a>').format(url=url, realm_str=realm_str)
|
||||
|
||||
|
||||
def realm_url_link(realm_str: str) -> Markup:
|
||||
host = Realm.host_for_subdomain(realm_str)
|
||||
url = settings.EXTERNAL_URI_SCHEME + mark_sanitized(host)
|
||||
url = get_realm(realm_str).uri
|
||||
return Markup('<a href="{url}"><i class="fa fa-home"></i></a>').format(url=url)
|
||||
|
||||
|
||||
def remote_installation_stats_link(server_id: int) -> Markup:
|
||||
def remote_installation_stats_link(server_id: int, hostname: str) -> Markup:
|
||||
from analytics.views.stats import stats_for_remote_installation
|
||||
|
||||
url = reverse(stats_for_remote_installation, kwargs=dict(remote_server_id=server_id))
|
||||
return Markup('<a href="{url}"><i class="fa fa-pie-chart"></i></a>').format(url=url)
|
||||
|
||||
|
||||
def remote_installation_support_link(hostname: str) -> Markup:
|
||||
support_url = reverse("remote_servers_support")
|
||||
query = urlencode({"q": hostname})
|
||||
url = append_url_query_string(support_url, query)
|
||||
return Markup('<a href="{url}"><i class="fa fa-gear"></i></a>').format(url=url)
|
||||
return Markup('<a href="{url}"><i class="fa fa-pie-chart"></i>{hostname}</a>').format(
|
||||
url=url, hostname=hostname
|
||||
)
|
||||
|
||||
|
||||
def get_user_activity_summary(records: Collection[UserActivity]) -> Dict[str, Any]:
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import itertools
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Dict, Optional
|
||||
from contextlib import suppress
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
@@ -8,54 +12,55 @@ from django.shortcuts import render
|
||||
from django.template import loader
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from markupsafe import Markup
|
||||
from psycopg2.sql import SQL
|
||||
from psycopg2.sql import SQL, Composable, Literal
|
||||
|
||||
from analytics.lib.counts import COUNT_STATS
|
||||
from analytics.views.activity_common import (
|
||||
dictfetchall,
|
||||
fix_rows,
|
||||
format_date_for_activity_reports,
|
||||
get_query_data,
|
||||
make_table,
|
||||
realm_activity_link,
|
||||
realm_stats_link,
|
||||
realm_support_link,
|
||||
realm_url_link,
|
||||
remote_installation_stats_link,
|
||||
)
|
||||
from analytics.views.support import get_plan_type_string
|
||||
from analytics.views.support import get_plan_name
|
||||
from zerver.decorator import require_server_admin
|
||||
from zerver.lib.request import has_request_variables
|
||||
from zerver.models import Realm
|
||||
from zerver.models.realms import get_org_type_display_name
|
||||
from zerver.lib.timestamp import timestamp_to_datetime
|
||||
from zerver.models import Realm, UserActivityInterval, get_org_type_display_name
|
||||
|
||||
if settings.BILLING_ENABLED:
|
||||
from corporate.lib.analytics import (
|
||||
from corporate.lib.stripe import (
|
||||
estimate_annual_recurring_revenue_by_realm,
|
||||
get_realms_with_default_discount_dict,
|
||||
get_realms_to_default_discount_dict,
|
||||
)
|
||||
|
||||
|
||||
def get_realm_day_counts() -> Dict[str, Dict[str, Markup]]:
|
||||
# To align with UTC days, we subtract an hour from end_time to
|
||||
# get the start_time, since the hour that starts at midnight was
|
||||
# on the previous day.
|
||||
query = SQL(
|
||||
"""
|
||||
select
|
||||
r.string_id,
|
||||
(now()::date - (end_time - interval '1 hour')::date) age,
|
||||
coalesce(sum(value), 0) cnt
|
||||
from zerver_realm r
|
||||
join analytics_realmcount rc on r.id = rc.realm_id
|
||||
(now()::date - date_sent::date) age,
|
||||
count(*) cnt
|
||||
from zerver_message m
|
||||
join zerver_userprofile up on up.id = m.sender_id
|
||||
join zerver_realm r on r.id = up.realm_id
|
||||
join zerver_client c on c.id = m.sending_client_id
|
||||
where
|
||||
property = 'messages_sent:is_bot:hour'
|
||||
(not up.is_bot)
|
||||
and
|
||||
subgroup = 'false'
|
||||
date_sent > now()::date - interval '8 day'
|
||||
and
|
||||
end_time > now()::date - interval '8 day' - interval '1 hour'
|
||||
c.name not in ('zephyr_mirror', 'ZulipMonitoring')
|
||||
group by
|
||||
r.string_id,
|
||||
age
|
||||
order by
|
||||
r.string_id,
|
||||
age
|
||||
"""
|
||||
)
|
||||
cursor = connection.cursor()
|
||||
@@ -67,31 +72,33 @@ def get_realm_day_counts() -> Dict[str, Dict[str, Markup]]:
|
||||
for row in rows:
|
||||
counts[row["string_id"]][row["age"]] = row["cnt"]
|
||||
|
||||
def format_count(cnt: int, style: Optional[str] = None) -> Markup:
|
||||
if style is not None:
|
||||
good_bad = style
|
||||
elif cnt == min_cnt:
|
||||
good_bad = "bad"
|
||||
elif cnt == max_cnt:
|
||||
good_bad = "good"
|
||||
else:
|
||||
good_bad = "neutral"
|
||||
|
||||
return Markup('<td class="number {good_bad}">{cnt}</td>').format(good_bad=good_bad, cnt=cnt)
|
||||
|
||||
result = {}
|
||||
for string_id in counts:
|
||||
raw_cnts = [counts[string_id].get(age, 0) for age in range(8)]
|
||||
min_cnt = min(raw_cnts[1:])
|
||||
max_cnt = max(raw_cnts[1:])
|
||||
|
||||
def format_count(cnt: int, style: Optional[str] = None) -> Markup:
|
||||
if style is not None:
|
||||
good_bad = style
|
||||
elif cnt == min_cnt:
|
||||
good_bad = "bad"
|
||||
elif cnt == max_cnt:
|
||||
good_bad = "good"
|
||||
else:
|
||||
good_bad = "neutral"
|
||||
|
||||
return Markup('<td class="number {good_bad}">{cnt}</td>').format(
|
||||
good_bad=good_bad, cnt=cnt
|
||||
)
|
||||
|
||||
cnts = format_count(raw_cnts[0], "neutral") + Markup().join(map(format_count, raw_cnts[1:]))
|
||||
result[string_id] = dict(cnts=cnts)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def realm_summary_table() -> str:
|
||||
def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
|
||||
now = timezone_now()
|
||||
|
||||
query = SQL(
|
||||
@@ -202,10 +209,10 @@ def realm_summary_table() -> str:
|
||||
total_arr = 0
|
||||
if settings.BILLING_ENABLED:
|
||||
estimated_arrs = estimate_annual_recurring_revenue_by_realm()
|
||||
realms_with_default_discount = get_realms_with_default_discount_dict()
|
||||
realms_to_default_discount = get_realms_to_default_discount_dict()
|
||||
|
||||
for row in rows:
|
||||
row["plan_type_string"] = get_plan_type_string(row["plan_type"])
|
||||
row["plan_type_string"] = get_plan_name(row["plan_type"])
|
||||
|
||||
string_id = row["string_id"]
|
||||
|
||||
@@ -213,14 +220,14 @@ def realm_summary_table() -> str:
|
||||
row["arr"] = estimated_arrs[string_id]
|
||||
|
||||
if row["plan_type"] in [Realm.PLAN_TYPE_STANDARD, Realm.PLAN_TYPE_PLUS]:
|
||||
row["effective_rate"] = 100 - int(realms_with_default_discount.get(string_id, 0))
|
||||
row["effective_rate"] = 100 - int(realms_to_default_discount.get(string_id, 0))
|
||||
elif row["plan_type"] == Realm.PLAN_TYPE_STANDARD_FREE:
|
||||
row["effective_rate"] = 0
|
||||
elif (
|
||||
row["plan_type"] == Realm.PLAN_TYPE_LIMITED
|
||||
and string_id in realms_with_default_discount
|
||||
and string_id in realms_to_default_discount
|
||||
):
|
||||
row["effective_rate"] = 100 - int(realms_with_default_discount[string_id])
|
||||
row["effective_rate"] = 100 - int(realms_to_default_discount[string_id])
|
||||
else:
|
||||
row["effective_rate"] = ""
|
||||
|
||||
@@ -229,6 +236,17 @@ def realm_summary_table() -> str:
|
||||
for row in rows:
|
||||
row["org_type_string"] = get_org_type_display_name(row["org_type"])
|
||||
|
||||
# augment data with realm_minutes
|
||||
total_hours = 0.0
|
||||
for row in rows:
|
||||
string_id = row["string_id"]
|
||||
minutes = realm_minutes.get(string_id, 0.0)
|
||||
hours = minutes / 60.0
|
||||
total_hours += hours
|
||||
row["hours"] = str(int(hours))
|
||||
with suppress(Exception):
|
||||
row["hours_per_user"] = "{:.1f}".format(hours / row["dau_count"])
|
||||
|
||||
# formatting
|
||||
for row in rows:
|
||||
row["realm_url"] = realm_url_link(row["string_id"])
|
||||
@@ -237,7 +255,10 @@ def realm_summary_table() -> str:
|
||||
row["string_id"] = realm_activity_link(row["string_id"])
|
||||
|
||||
# Count active sites
|
||||
num_active_sites = sum(row["dau_count"] >= 5 for row in rows)
|
||||
def meets_goal(row: Dict[str, int]) -> bool:
|
||||
return row["dau_count"] >= 5
|
||||
|
||||
num_active_sites = len(list(filter(meets_goal, rows)))
|
||||
|
||||
# create totals
|
||||
total_dau_count = 0
|
||||
@@ -263,6 +284,7 @@ def realm_summary_table() -> str:
|
||||
dau_count=total_dau_count,
|
||||
user_profile_count=total_user_profile_count,
|
||||
bot_count=total_bot_count,
|
||||
hours=int(total_hours),
|
||||
wau_count=total_wau_count,
|
||||
)
|
||||
|
||||
@@ -280,21 +302,218 @@ def realm_summary_table() -> str:
|
||||
return content
|
||||
|
||||
|
||||
@require_server_admin
|
||||
@has_request_variables
|
||||
def get_installation_activity(request: HttpRequest) -> HttpResponse:
|
||||
content: str = realm_summary_table()
|
||||
title = "Installation activity"
|
||||
def user_activity_intervals() -> Tuple[Markup, Dict[str, float]]:
|
||||
day_end = timestamp_to_datetime(time.time())
|
||||
day_start = day_end - timedelta(hours=24)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"analytics/activity_details_template.html",
|
||||
context=dict(data=content, title=title, is_home=True),
|
||||
output = Markup()
|
||||
output += "Per-user online duration for the last 24 hours:\n"
|
||||
total_duration = timedelta(0)
|
||||
|
||||
all_intervals = (
|
||||
UserActivityInterval.objects.filter(
|
||||
end__gte=day_start,
|
||||
start__lte=day_end,
|
||||
)
|
||||
.select_related(
|
||||
"user_profile",
|
||||
"user_profile__realm",
|
||||
)
|
||||
.only(
|
||||
"start",
|
||||
"end",
|
||||
"user_profile__delivery_email",
|
||||
"user_profile__realm__string_id",
|
||||
)
|
||||
.order_by(
|
||||
"user_profile__realm__string_id",
|
||||
"user_profile__delivery_email",
|
||||
)
|
||||
)
|
||||
|
||||
by_string_id = lambda row: row.user_profile.realm.string_id
|
||||
by_email = lambda row: row.user_profile.delivery_email
|
||||
|
||||
realm_minutes = {}
|
||||
|
||||
for string_id, realm_intervals in itertools.groupby(all_intervals, by_string_id):
|
||||
realm_duration = timedelta(0)
|
||||
output += Markup("<hr>") + f"{string_id}\n"
|
||||
for email, intervals in itertools.groupby(realm_intervals, by_email):
|
||||
duration = timedelta(0)
|
||||
for interval in intervals:
|
||||
start = max(day_start, interval.start)
|
||||
end = min(day_end, interval.end)
|
||||
duration += end - start
|
||||
|
||||
total_duration += duration
|
||||
realm_duration += duration
|
||||
output += f" {email:<37}{duration}\n"
|
||||
|
||||
realm_minutes[string_id] = realm_duration.total_seconds() / 60
|
||||
|
||||
output += f"\nTotal duration: {total_duration}\n"
|
||||
output += f"\nTotal duration in minutes: {total_duration.total_seconds() / 60.}\n"
|
||||
output += f"Total duration amortized to a month: {total_duration.total_seconds() * 30. / 60.}"
|
||||
content = Markup("<pre>{}</pre>").format(output)
|
||||
return content, realm_minutes
|
||||
|
||||
|
||||
def ad_hoc_queries() -> List[Dict[str, str]]:
|
||||
def get_page(
|
||||
query: Composable, cols: Sequence[str], title: str, totals_columns: Sequence[int] = []
|
||||
) -> Dict[str, str]:
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(query)
|
||||
rows = cursor.fetchall()
|
||||
rows = list(map(list, rows))
|
||||
cursor.close()
|
||||
|
||||
def fix_rows(
|
||||
i: int, fixup_func: Union[Callable[[str], Markup], Callable[[datetime], str]]
|
||||
) -> None:
|
||||
for row in rows:
|
||||
row[i] = fixup_func(row[i])
|
||||
|
||||
total_row = []
|
||||
for i, col in enumerate(cols):
|
||||
if col == "Realm":
|
||||
fix_rows(i, realm_activity_link)
|
||||
elif col in ["Last time", "Last visit"]:
|
||||
fix_rows(i, format_date_for_activity_reports)
|
||||
elif col == "Hostname":
|
||||
for row in rows:
|
||||
row[i] = remote_installation_stats_link(row[0], row[i])
|
||||
if len(totals_columns) > 0:
|
||||
if i == 0:
|
||||
total_row.append("Total")
|
||||
elif i in totals_columns:
|
||||
total_row.append(str(sum(row[i] for row in rows if row[i] is not None)))
|
||||
else:
|
||||
total_row.append("")
|
||||
if len(totals_columns) > 0:
|
||||
rows.insert(0, total_row)
|
||||
|
||||
content = make_table(title, cols, rows)
|
||||
|
||||
return dict(
|
||||
content=content,
|
||||
title=title,
|
||||
)
|
||||
|
||||
pages = []
|
||||
|
||||
###
|
||||
|
||||
for mobile_type in ["Android", "ZulipiOS"]:
|
||||
title = f"{mobile_type} usage"
|
||||
|
||||
query: Composable = SQL(
|
||||
"""
|
||||
select
|
||||
realm.string_id,
|
||||
up.id user_id,
|
||||
client.name,
|
||||
sum(count) as hits,
|
||||
max(last_visit) as last_time
|
||||
from zerver_useractivity ua
|
||||
join zerver_client client on client.id = ua.client_id
|
||||
join zerver_userprofile up on up.id = ua.user_profile_id
|
||||
join zerver_realm realm on realm.id = up.realm_id
|
||||
where
|
||||
client.name like {mobile_type}
|
||||
group by string_id, up.id, client.name
|
||||
having max(last_visit) > now() - interval '2 week'
|
||||
order by string_id, up.id, client.name
|
||||
"""
|
||||
).format(
|
||||
mobile_type=Literal(mobile_type),
|
||||
)
|
||||
|
||||
cols = [
|
||||
"Realm",
|
||||
"User id",
|
||||
"Name",
|
||||
"Hits",
|
||||
"Last time",
|
||||
]
|
||||
|
||||
pages.append(get_page(query, cols, title))
|
||||
|
||||
###
|
||||
|
||||
title = "Desktop users"
|
||||
|
||||
query = SQL(
|
||||
"""
|
||||
select
|
||||
realm.string_id,
|
||||
client.name,
|
||||
sum(count) as hits,
|
||||
max(last_visit) as last_time
|
||||
from zerver_useractivity ua
|
||||
join zerver_client client on client.id = ua.client_id
|
||||
join zerver_userprofile up on up.id = ua.user_profile_id
|
||||
join zerver_realm realm on realm.id = up.realm_id
|
||||
where
|
||||
client.name like 'desktop%%'
|
||||
group by string_id, client.name
|
||||
having max(last_visit) > now() - interval '2 week'
|
||||
order by string_id, client.name
|
||||
"""
|
||||
)
|
||||
|
||||
cols = [
|
||||
"Realm",
|
||||
"Client",
|
||||
"Hits",
|
||||
"Last time",
|
||||
]
|
||||
|
||||
pages.append(get_page(query, cols, title))
|
||||
|
||||
###
|
||||
|
||||
title = "Integrations by realm"
|
||||
|
||||
query = SQL(
|
||||
"""
|
||||
select
|
||||
realm.string_id,
|
||||
case
|
||||
when query like '%%external%%' then split_part(query, '/', 5)
|
||||
else client.name
|
||||
end client_name,
|
||||
sum(count) as hits,
|
||||
max(last_visit) as last_time
|
||||
from zerver_useractivity ua
|
||||
join zerver_client client on client.id = ua.client_id
|
||||
join zerver_userprofile up on up.id = ua.user_profile_id
|
||||
join zerver_realm realm on realm.id = up.realm_id
|
||||
where
|
||||
(query in ('send_message_backend', '/api/v1/send_message')
|
||||
and client.name not in ('Android', 'ZulipiOS')
|
||||
and client.name not like 'test: Zulip%%'
|
||||
)
|
||||
or
|
||||
query like '%%external%%'
|
||||
group by string_id, client_name
|
||||
having max(last_visit) > now() - interval '2 week'
|
||||
order by string_id, client_name
|
||||
"""
|
||||
)
|
||||
|
||||
cols = [
|
||||
"Realm",
|
||||
"Client",
|
||||
"Hits",
|
||||
"Last time",
|
||||
]
|
||||
|
||||
pages.append(get_page(query, cols, title))
|
||||
|
||||
###
|
||||
|
||||
@require_server_admin
|
||||
def get_integrations_activity(request: HttpRequest) -> HttpResponse:
|
||||
title = "Integrations by client"
|
||||
|
||||
query = SQL(
|
||||
@@ -331,20 +550,71 @@ def get_integrations_activity(request: HttpRequest) -> HttpResponse:
|
||||
"Last time",
|
||||
]
|
||||
|
||||
rows = get_query_data(query)
|
||||
for i, col in enumerate(cols):
|
||||
if col == "Realm":
|
||||
fix_rows(rows, i, realm_activity_link)
|
||||
elif col == "Last time":
|
||||
fix_rows(rows, i, format_date_for_activity_reports)
|
||||
pages.append(get_page(query, cols, title))
|
||||
|
||||
title = "Remote Zulip servers"
|
||||
|
||||
query = SQL(
|
||||
"""
|
||||
with icount as (
|
||||
select
|
||||
server_id,
|
||||
max(value) as max_value,
|
||||
max(end_time) as max_end_time
|
||||
from zilencer_remoteinstallationcount
|
||||
where
|
||||
property='active_users:is_bot:day'
|
||||
and subgroup='false'
|
||||
group by server_id
|
||||
),
|
||||
remote_push_devices as (
|
||||
select server_id, count(distinct(user_id)) as push_user_count from zilencer_remotepushdevicetoken
|
||||
group by server_id
|
||||
)
|
||||
select
|
||||
rserver.id,
|
||||
rserver.hostname,
|
||||
rserver.contact_email,
|
||||
max_value,
|
||||
push_user_count,
|
||||
max_end_time
|
||||
from zilencer_remotezulipserver rserver
|
||||
left join icount on icount.server_id = rserver.id
|
||||
left join remote_push_devices on remote_push_devices.server_id = rserver.id
|
||||
order by max_value DESC NULLS LAST, push_user_count DESC NULLS LAST
|
||||
"""
|
||||
)
|
||||
|
||||
cols = [
|
||||
"ID",
|
||||
"Hostname",
|
||||
"Contact email",
|
||||
"Analytics users",
|
||||
"Mobile users",
|
||||
"Last update time",
|
||||
]
|
||||
|
||||
pages.append(get_page(query, cols, title, totals_columns=[3, 4]))
|
||||
|
||||
return pages
|
||||
|
||||
|
||||
@require_server_admin
|
||||
@has_request_variables
|
||||
def get_installation_activity(request: HttpRequest) -> HttpResponse:
|
||||
duration_content, realm_minutes = user_activity_intervals()
|
||||
counts_content: str = realm_summary_table(realm_minutes)
|
||||
data = [
|
||||
("Counts", counts_content),
|
||||
("Durations", duration_content),
|
||||
]
|
||||
for page in ad_hoc_queries():
|
||||
data.append((page["title"], page["content"]))
|
||||
|
||||
title = "Activity"
|
||||
|
||||
content = make_table(title, cols, rows)
|
||||
return render(
|
||||
request,
|
||||
"analytics/activity_details_template.html",
|
||||
context=dict(
|
||||
data=content,
|
||||
title=title,
|
||||
is_home=False,
|
||||
),
|
||||
"analytics/activity.html",
|
||||
context=dict(data=data, title=title, is_home=True),
|
||||
)
|
||||
|
||||
@@ -163,13 +163,12 @@ def sent_messages_report(realm: str) -> str:
|
||||
"Bots",
|
||||
]
|
||||
|
||||
# Uses index: zerver_message_realm_date_sent
|
||||
query = SQL(
|
||||
"""
|
||||
select
|
||||
series.day::date,
|
||||
user_messages.humans,
|
||||
user_messages.bots
|
||||
humans.cnt,
|
||||
bots.cnt
|
||||
from (
|
||||
select generate_series(
|
||||
(now()::date - interval '2 week'),
|
||||
@@ -180,27 +179,45 @@ def sent_messages_report(realm: str) -> str:
|
||||
left join (
|
||||
select
|
||||
date_sent::date date_sent,
|
||||
count(*) filter (where not up.is_bot) as humans,
|
||||
count(*) filter (where up.is_bot) as bots
|
||||
count(*) cnt
|
||||
from zerver_message m
|
||||
join zerver_userprofile up on up.id = m.sender_id
|
||||
join zerver_realm r on r.id = up.realm_id
|
||||
where
|
||||
r.string_id = %s
|
||||
and
|
||||
date_sent > now() - interval '2 week'
|
||||
(not up.is_bot)
|
||||
and
|
||||
m.realm_id = r.id
|
||||
date_sent > now() - interval '2 week'
|
||||
group by
|
||||
date_sent::date
|
||||
order by
|
||||
date_sent::date
|
||||
) user_messages on
|
||||
series.day = user_messages.date_sent
|
||||
) humans on
|
||||
series.day = humans.date_sent
|
||||
left join (
|
||||
select
|
||||
date_sent::date date_sent,
|
||||
count(*) cnt
|
||||
from zerver_message m
|
||||
join zerver_userprofile up on up.id = m.sender_id
|
||||
join zerver_realm r on r.id = up.realm_id
|
||||
where
|
||||
r.string_id = %s
|
||||
and
|
||||
up.is_bot
|
||||
and
|
||||
date_sent > now() - interval '2 week'
|
||||
group by
|
||||
date_sent::date
|
||||
order by
|
||||
date_sent::date
|
||||
) bots on
|
||||
series.day = bots.date_sent
|
||||
"""
|
||||
)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(query, [realm])
|
||||
cursor.execute(query, [realm, realm])
|
||||
rows = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from psycopg2.sql import SQL
|
||||
|
||||
from analytics.views.activity_common import (
|
||||
fix_rows,
|
||||
format_date_for_activity_reports,
|
||||
get_query_data,
|
||||
make_table,
|
||||
remote_installation_stats_link,
|
||||
remote_installation_support_link,
|
||||
)
|
||||
from corporate.lib.analytics import get_plan_data_by_remote_server
|
||||
from zerver.decorator import require_server_admin
|
||||
from zilencer.models import get_remote_server_guest_and_non_guest_count
|
||||
|
||||
|
||||
@require_server_admin
|
||||
def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
|
||||
title = "Remote servers"
|
||||
|
||||
query = SQL(
|
||||
"""
|
||||
with icount_id as (
|
||||
select
|
||||
server_id,
|
||||
max(id) as max_count_id
|
||||
from zilencer_remoteinstallationcount
|
||||
where
|
||||
property='active_users:is_bot:day'
|
||||
and subgroup='false'
|
||||
group by server_id
|
||||
),
|
||||
icount as (
|
||||
select
|
||||
icount_id.server_id,
|
||||
value as latest_value,
|
||||
end_time as latest_end_time
|
||||
from icount_id
|
||||
join zilencer_remoteinstallationcount
|
||||
on max_count_id = zilencer_remoteinstallationcount.id
|
||||
),
|
||||
mobile_push_forwarded_count as (
|
||||
select
|
||||
server_id,
|
||||
sum(coalesce(value, 0)) as push_forwarded_count
|
||||
from zilencer_remoteinstallationcount
|
||||
where
|
||||
property = 'mobile_pushes_forwarded::day'
|
||||
and end_time >= current_timestamp(0) - interval '7 days'
|
||||
group by server_id
|
||||
),
|
||||
remote_push_devices as (
|
||||
select
|
||||
server_id,
|
||||
count(distinct(user_id, user_uuid)) as push_user_count
|
||||
from zilencer_remotepushdevicetoken
|
||||
group by server_id
|
||||
)
|
||||
select
|
||||
rserver.id,
|
||||
rserver.hostname,
|
||||
rserver.contact_email,
|
||||
rserver.last_version,
|
||||
latest_value,
|
||||
push_user_count,
|
||||
latest_end_time,
|
||||
push_forwarded_count
|
||||
from zilencer_remotezulipserver rserver
|
||||
left join icount on icount.server_id = rserver.id
|
||||
left join mobile_push_forwarded_count on mobile_push_forwarded_count.server_id = rserver.id
|
||||
left join remote_push_devices on remote_push_devices.server_id = rserver.id
|
||||
where not deactivated
|
||||
order by latest_value DESC NULLS LAST, push_user_count DESC NULLS LAST
|
||||
"""
|
||||
)
|
||||
|
||||
cols = [
|
||||
"ID",
|
||||
"Hostname",
|
||||
"Contact email",
|
||||
"Zulip version",
|
||||
"Analytics users",
|
||||
"Mobile users",
|
||||
"Last update time",
|
||||
"Mobile pushes forwarded",
|
||||
"Plan name",
|
||||
"Plan status",
|
||||
"ARR",
|
||||
"Non guest users",
|
||||
"Guest users",
|
||||
"Links",
|
||||
]
|
||||
|
||||
rows = get_query_data(query)
|
||||
total_row = []
|
||||
totals_columns = [4, 5]
|
||||
plan_data_by_remote_server = get_plan_data_by_remote_server()
|
||||
|
||||
for row in rows:
|
||||
# Add estimated revenue for server
|
||||
server_plan_data = plan_data_by_remote_server.get(row[0])
|
||||
if server_plan_data is None:
|
||||
row.append("---")
|
||||
row.append("---")
|
||||
row.append("---")
|
||||
else:
|
||||
row.append(server_plan_data.current_plan_name)
|
||||
row.append(server_plan_data.current_status)
|
||||
row.append(server_plan_data.annual_revenue)
|
||||
# Add user counts
|
||||
remote_server_counts = get_remote_server_guest_and_non_guest_count(row[0])
|
||||
row.append(remote_server_counts.non_guest_user_count)
|
||||
row.append(remote_server_counts.guest_user_count)
|
||||
# Add links
|
||||
stats = remote_installation_stats_link(row[0])
|
||||
support = remote_installation_support_link(row[1])
|
||||
links = stats + " " + support
|
||||
row.append(links)
|
||||
for i, col in enumerate(cols):
|
||||
if col == "Last update time":
|
||||
fix_rows(rows, i, format_date_for_activity_reports)
|
||||
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("")
|
||||
rows.insert(0, total_row)
|
||||
|
||||
content = make_table(title, cols, rows)
|
||||
return render(
|
||||
request,
|
||||
"analytics/activity_details_template.html",
|
||||
context=dict(data=content, title=title, is_home=False),
|
||||
)
|
||||
@@ -10,7 +10,6 @@ from django.shortcuts import render
|
||||
from django.utils import translation
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from django.utils.translation import gettext as _
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from analytics.lib.counts import COUNT_STATS, CountStat
|
||||
from analytics.lib.time_utils import time_range
|
||||
@@ -33,11 +32,9 @@ from zerver.lib.exceptions import JsonableError
|
||||
from zerver.lib.i18n import get_and_set_request_language, get_language_translation_data
|
||||
from zerver.lib.request import REQ, has_request_variables
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.streams import access_stream_by_id
|
||||
from zerver.lib.timestamp import convert_to_UTC
|
||||
from zerver.lib.validator import to_non_negative_int
|
||||
from zerver.models import Client, Realm, Stream, UserProfile
|
||||
from zerver.models.realms import get_realm
|
||||
from zerver.models import Client, Realm, UserProfile, get_realm
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
from zilencer.models import RemoteInstallationCount, RemoteRealmCount, RemoteZulipServer
|
||||
@@ -158,21 +155,6 @@ def get_chart_data_for_realm(
|
||||
return get_chart_data(request, user_profile, realm=realm, **kwargs)
|
||||
|
||||
|
||||
@require_non_guest_user
|
||||
@has_request_variables
|
||||
def get_chart_data_for_stream(
|
||||
request: HttpRequest, /, user_profile: UserProfile, stream_id: int
|
||||
) -> HttpResponse:
|
||||
stream, ignored_sub = access_stream_by_id(
|
||||
user_profile,
|
||||
stream_id,
|
||||
require_active=True,
|
||||
allow_realm_admin=True,
|
||||
)
|
||||
|
||||
return get_chart_data(request, user_profile, stream=stream)
|
||||
|
||||
|
||||
@require_server_admin_api
|
||||
@has_request_variables
|
||||
def get_chart_data_for_remote_realm(
|
||||
@@ -254,17 +236,13 @@ def get_chart_data(
|
||||
min_length: Optional[int] = REQ(converter=to_non_negative_int, default=None),
|
||||
start: Optional[datetime] = REQ(converter=to_utc_datetime, default=None),
|
||||
end: Optional[datetime] = REQ(converter=to_utc_datetime, default=None),
|
||||
# These last several parameters are only used by functions
|
||||
# wrapping get_chart_data; the callers are responsible for
|
||||
# parsing/validation/authorization for them.
|
||||
realm: Optional[Realm] = None,
|
||||
for_installation: bool = False,
|
||||
remote: bool = False,
|
||||
remote_realm_id: Optional[int] = None,
|
||||
server: Optional["RemoteZulipServer"] = None,
|
||||
stream: Optional[Stream] = None,
|
||||
) -> HttpResponse:
|
||||
TableType: TypeAlias = Union[
|
||||
TableType = Union[
|
||||
Type["RemoteInstallationCount"],
|
||||
Type[InstallationCount],
|
||||
Type["RemoteRealmCount"],
|
||||
@@ -286,9 +264,7 @@ def get_chart_data(
|
||||
else:
|
||||
aggregate_table = RealmCount
|
||||
|
||||
tables: Union[
|
||||
Tuple[TableType], Tuple[TableType, Type[UserCount]], Tuple[TableType, Type[StreamCount]]
|
||||
]
|
||||
tables: Union[Tuple[TableType], Tuple[TableType, Type[UserCount]]]
|
||||
|
||||
if chart_name == "number_of_humans":
|
||||
stats = [
|
||||
@@ -338,18 +314,8 @@ def get_chart_data(
|
||||
subgroup_to_label = {stats[0]: {None: "read"}}
|
||||
labels_sort_function = None
|
||||
include_empty_subgroups = True
|
||||
elif chart_name == "messages_sent_by_stream":
|
||||
if stream is None:
|
||||
raise JsonableError(
|
||||
_("Missing stream for chart: {chart_name}").format(chart_name=chart_name)
|
||||
)
|
||||
stats = [COUNT_STATS["messages_in_stream:is_bot:day"]]
|
||||
tables = (aggregate_table, StreamCount)
|
||||
subgroup_to_label = {stats[0]: {"false": "human", "true": "bot"}}
|
||||
labels_sort_function = None
|
||||
include_empty_subgroups = True
|
||||
else:
|
||||
raise JsonableError(_("Unknown chart name: {chart_name}").format(chart_name=chart_name))
|
||||
raise JsonableError(_("Unknown chart name: {}").format(chart_name))
|
||||
|
||||
# Most likely someone using our API endpoint. The /stats page does not
|
||||
# pass a start or end in its requests.
|
||||
@@ -430,7 +396,6 @@ def get_chart_data(
|
||||
InstallationCount: "everyone",
|
||||
RealmCount: "everyone",
|
||||
UserCount: "user",
|
||||
StreamCount: "everyone",
|
||||
}
|
||||
if settings.ZILENCER_ENABLED:
|
||||
aggregation_level[RemoteInstallationCount] = "everyone"
|
||||
@@ -442,9 +407,6 @@ def get_chart_data(
|
||||
RealmCount: realm.id,
|
||||
UserCount: user_profile.id,
|
||||
}
|
||||
if stream is not None:
|
||||
id_value[StreamCount] = stream.id
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
if server is not None:
|
||||
id_value[RemoteInstallationCount] = server.id
|
||||
@@ -475,7 +437,8 @@ def get_chart_data(
|
||||
|
||||
|
||||
def sort_by_totals(value_arrays: Dict[str, List[int]]) -> List[str]:
|
||||
totals = sorted(((sum(values), label) for label, values in value_arrays.items()), reverse=True)
|
||||
totals = [(sum(values), label) for label, values in value_arrays.items()]
|
||||
totals.sort(reverse=True)
|
||||
return [label for total, label in totals]
|
||||
|
||||
|
||||
@@ -501,17 +464,17 @@ CountT = TypeVar("CountT", bound=BaseCount)
|
||||
|
||||
def table_filtered_to_id(table: Type[CountT], key_id: int) -> QuerySet[CountT]:
|
||||
if table == RealmCount:
|
||||
return table._default_manager.filter(realm_id=key_id)
|
||||
return table.objects.filter(realm_id=key_id)
|
||||
elif table == UserCount:
|
||||
return table._default_manager.filter(user_id=key_id)
|
||||
return table.objects.filter(user_id=key_id)
|
||||
elif table == StreamCount:
|
||||
return table._default_manager.filter(stream_id=key_id)
|
||||
return table.objects.filter(stream_id=key_id)
|
||||
elif table == InstallationCount:
|
||||
return table._default_manager.all()
|
||||
return table.objects.all()
|
||||
elif settings.ZILENCER_ENABLED and table == RemoteInstallationCount:
|
||||
return table._default_manager.filter(server_id=key_id)
|
||||
return table.objects.filter(server_id=key_id)
|
||||
elif settings.ZILENCER_ENABLED and table == RemoteRealmCount:
|
||||
return table._default_manager.filter(realm_id=key_id)
|
||||
return table.objects.filter(realm_id=key_id)
|
||||
else:
|
||||
raise AssertionError(f"Unknown table: {table}")
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import urllib
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, Iterable, List, Optional, Union
|
||||
from urllib.parse import urlencode, urlsplit
|
||||
from typing import Any, Dict, Iterable, List, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -40,47 +42,41 @@ from zerver.models import (
|
||||
Realm,
|
||||
RealmReactivationStatus,
|
||||
UserProfile,
|
||||
get_org_type_display_name,
|
||||
get_realm,
|
||||
get_user_profile_by_id,
|
||||
)
|
||||
from zerver.models.realms import get_org_type_display_name, get_realm
|
||||
from zerver.models.users import get_user_profile_by_id
|
||||
from zerver.views.invite import get_invitee_emails_set
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
from zilencer.lib.remote_counts import MissingDataError, compute_max_monthly_messages
|
||||
from zilencer.models import RemoteRealm, RemoteZulipServer
|
||||
|
||||
if settings.BILLING_ENABLED:
|
||||
from corporate.lib.stripe import approve_sponsorship as do_approve_sponsorship
|
||||
from corporate.lib.stripe import (
|
||||
RealmBillingSession,
|
||||
RemoteRealmBillingSession,
|
||||
RemoteServerBillingSession,
|
||||
SupportType,
|
||||
SupportViewRequest,
|
||||
attach_discount_to_realm,
|
||||
downgrade_at_the_end_of_billing_cycle,
|
||||
downgrade_now_without_creating_additional_invoices,
|
||||
get_discount_for_realm,
|
||||
get_latest_seat_count,
|
||||
make_end_of_cycle_updates_if_needed,
|
||||
switch_realm_from_standard_to_plus_plan,
|
||||
update_billing_method_of_current_plan,
|
||||
update_sponsorship_status,
|
||||
void_all_open_invoices,
|
||||
)
|
||||
from corporate.lib.support import (
|
||||
PlanData,
|
||||
SupportData,
|
||||
get_current_plan_data_for_support_view,
|
||||
get_customer_discount_for_support_view,
|
||||
get_data_for_support_view,
|
||||
from corporate.models import (
|
||||
Customer,
|
||||
CustomerPlan,
|
||||
get_current_plan_by_realm,
|
||||
get_customer_by_realm,
|
||||
)
|
||||
from corporate.models import CustomerPlan
|
||||
|
||||
|
||||
def get_plan_type_string(plan_type: int) -> str:
|
||||
def get_plan_name(plan_type: int) -> str:
|
||||
return {
|
||||
Realm.PLAN_TYPE_SELF_HOSTED: "Self-hosted",
|
||||
Realm.PLAN_TYPE_LIMITED: "Limited",
|
||||
Realm.PLAN_TYPE_STANDARD: "Standard",
|
||||
Realm.PLAN_TYPE_STANDARD_FREE: "Standard free",
|
||||
Realm.PLAN_TYPE_PLUS: "Plus",
|
||||
RemoteZulipServer.PLAN_TYPE_SELF_MANAGED: "Self-managed",
|
||||
RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY: CustomerPlan.name_from_tier(
|
||||
CustomerPlan.TIER_SELF_HOSTED_LEGACY
|
||||
),
|
||||
RemoteZulipServer.PLAN_TYPE_COMMUNITY: "Community",
|
||||
RemoteZulipServer.PLAN_TYPE_BUSINESS: "Business",
|
||||
RemoteZulipServer.PLAN_TYPE_ENTERPRISE: "Enterprise",
|
||||
Realm.PLAN_TYPE_SELF_HOSTED: "self-hosted",
|
||||
Realm.PLAN_TYPE_LIMITED: "limited",
|
||||
Realm.PLAN_TYPE_STANDARD: "standard",
|
||||
Realm.PLAN_TYPE_STANDARD_FREE: "open source",
|
||||
Realm.PLAN_TYPE_PLUS: "plus",
|
||||
}[plan_type]
|
||||
|
||||
|
||||
@@ -133,7 +129,7 @@ VALID_MODIFY_PLAN_METHODS = [
|
||||
"downgrade_at_billing_cycle_end",
|
||||
"downgrade_now_without_additional_licenses",
|
||||
"downgrade_now_void_open_invoices",
|
||||
"upgrade_plan_tier",
|
||||
"upgrade_to_plus",
|
||||
]
|
||||
|
||||
VALID_STATUS_VALUES = [
|
||||
@@ -141,12 +137,20 @@ VALID_STATUS_VALUES = [
|
||||
"deactivated",
|
||||
]
|
||||
|
||||
VALID_BILLING_MODALITY_VALUES = [
|
||||
VALID_BILLING_METHODS = [
|
||||
"send_invoice",
|
||||
"charge_automatically",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanData:
|
||||
customer: Optional["Customer"] = None
|
||||
current_plan: Optional["CustomerPlan"] = None
|
||||
licenses: Optional[int] = None
|
||||
licenses_used: Optional[int] = None
|
||||
|
||||
|
||||
@require_server_admin
|
||||
@has_request_variables
|
||||
def support(
|
||||
@@ -156,8 +160,8 @@ def support(
|
||||
discount: Optional[Decimal] = REQ(default=None, converter=to_decimal),
|
||||
new_subdomain: Optional[str] = REQ(default=None),
|
||||
status: Optional[str] = REQ(default=None, str_validator=check_string_in(VALID_STATUS_VALUES)),
|
||||
billing_modality: Optional[str] = REQ(
|
||||
default=None, str_validator=check_string_in(VALID_BILLING_MODALITY_VALUES)
|
||||
billing_method: Optional[str] = REQ(
|
||||
default=None, str_validator=check_string_in(VALID_BILLING_METHODS)
|
||||
),
|
||||
sponsorship_pending: Optional[bool] = REQ(default=None, json_validator=check_bool),
|
||||
approve_sponsorship: bool = REQ(default=False, json_validator=check_bool),
|
||||
@@ -175,8 +179,6 @@ def support(
|
||||
context["success_message"] = request.session["success_message"]
|
||||
del request.session["success_message"]
|
||||
|
||||
acting_user = request.user
|
||||
assert isinstance(acting_user, UserProfile)
|
||||
if settings.BILLING_ENABLED and request.method == "POST":
|
||||
# We check that request.POST only has two keys in it: The
|
||||
# realm_id and a field to change.
|
||||
@@ -189,42 +191,24 @@ def support(
|
||||
assert realm_id is not None
|
||||
realm = Realm.objects.get(id=realm_id)
|
||||
|
||||
support_view_request = None
|
||||
|
||||
if approve_sponsorship:
|
||||
support_view_request = SupportViewRequest(support_type=SupportType.approve_sponsorship)
|
||||
elif sponsorship_pending is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.update_sponsorship_status,
|
||||
sponsorship_status=sponsorship_pending,
|
||||
)
|
||||
elif discount is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.attach_discount,
|
||||
discount=discount,
|
||||
)
|
||||
elif billing_modality is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.update_billing_modality,
|
||||
billing_modality=billing_modality,
|
||||
)
|
||||
elif modify_plan is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.modify_plan,
|
||||
plan_modification=modify_plan,
|
||||
)
|
||||
if modify_plan == "upgrade_plan_tier":
|
||||
support_view_request["new_plan_tier"] = CustomerPlan.TIER_CLOUD_PLUS
|
||||
elif plan_type is not None:
|
||||
acting_user = request.user
|
||||
assert isinstance(acting_user, UserProfile)
|
||||
if plan_type is not None:
|
||||
current_plan_type = realm.plan_type
|
||||
do_change_realm_plan_type(realm, plan_type, acting_user=acting_user)
|
||||
msg = f"Plan type of {realm.string_id} changed from {get_plan_type_string(current_plan_type)} to {get_plan_type_string(plan_type)} "
|
||||
msg = f"Plan type of {realm.string_id} changed from {get_plan_name(current_plan_type)} to {get_plan_name(plan_type)} "
|
||||
context["success_message"] = msg
|
||||
elif org_type is not None:
|
||||
current_realm_type = realm.org_type
|
||||
do_change_realm_org_type(realm, org_type, acting_user=acting_user)
|
||||
msg = f"Org type of {realm.string_id} changed from {get_org_type_display_name(current_realm_type)} to {get_org_type_display_name(org_type)} "
|
||||
context["success_message"] = msg
|
||||
elif discount is not None:
|
||||
current_discount = get_discount_for_realm(realm) or 0
|
||||
attach_discount_to_realm(realm, discount, acting_user=acting_user)
|
||||
context[
|
||||
"success_message"
|
||||
] = f"Discount of {realm.string_id} changed to {discount}% from {current_discount}%."
|
||||
elif new_subdomain is not None:
|
||||
old_subdomain = realm.string_id
|
||||
try:
|
||||
@@ -248,6 +232,51 @@ def support(
|
||||
elif status == "deactivated":
|
||||
do_deactivate_realm(realm, acting_user=acting_user)
|
||||
context["success_message"] = f"{realm.string_id} deactivated."
|
||||
elif billing_method is not None:
|
||||
if billing_method == "send_invoice":
|
||||
update_billing_method_of_current_plan(
|
||||
realm, charge_automatically=False, acting_user=acting_user
|
||||
)
|
||||
context[
|
||||
"success_message"
|
||||
] = f"Billing method of {realm.string_id} updated to pay by invoice."
|
||||
elif billing_method == "charge_automatically":
|
||||
update_billing_method_of_current_plan(
|
||||
realm, charge_automatically=True, acting_user=acting_user
|
||||
)
|
||||
context[
|
||||
"success_message"
|
||||
] = f"Billing method of {realm.string_id} updated to charge automatically."
|
||||
elif sponsorship_pending is not None:
|
||||
if sponsorship_pending:
|
||||
update_sponsorship_status(realm, True, acting_user=acting_user)
|
||||
context["success_message"] = f"{realm.string_id} marked as pending sponsorship."
|
||||
else:
|
||||
update_sponsorship_status(realm, False, acting_user=acting_user)
|
||||
context["success_message"] = f"{realm.string_id} is no longer pending sponsorship."
|
||||
elif approve_sponsorship:
|
||||
do_approve_sponsorship(realm, acting_user=acting_user)
|
||||
context["success_message"] = f"Sponsorship approved for {realm.string_id}"
|
||||
elif modify_plan is not None:
|
||||
if modify_plan == "downgrade_at_billing_cycle_end":
|
||||
downgrade_at_the_end_of_billing_cycle(realm)
|
||||
context[
|
||||
"success_message"
|
||||
] = f"{realm.string_id} marked for downgrade at the end of billing cycle"
|
||||
elif modify_plan == "downgrade_now_without_additional_licenses":
|
||||
downgrade_now_without_creating_additional_invoices(realm)
|
||||
context[
|
||||
"success_message"
|
||||
] = f"{realm.string_id} downgraded without creating additional invoices"
|
||||
elif modify_plan == "downgrade_now_void_open_invoices":
|
||||
downgrade_now_without_creating_additional_invoices(realm)
|
||||
voided_invoices_count = void_all_open_invoices(realm)
|
||||
context[
|
||||
"success_message"
|
||||
] = f"{realm.string_id} downgraded and voided {voided_invoices_count} open invoices"
|
||||
elif modify_plan == "upgrade_to_plus":
|
||||
switch_realm_from_standard_to_plus_plan(realm)
|
||||
context["success_message"] = f"{realm.string_id} upgraded to Plus"
|
||||
elif scrub_realm:
|
||||
do_scrub_realm(realm, acting_user=acting_user)
|
||||
context["success_message"] = f"{realm.string_id} scrubbed."
|
||||
@@ -258,13 +287,6 @@ def support(
|
||||
do_delete_user_preserving_messages(user_profile_for_deletion)
|
||||
context["success_message"] = f"{user_email} in {realm.subdomain} deleted."
|
||||
|
||||
if support_view_request is not None:
|
||||
billing_session = RealmBillingSession(
|
||||
user=acting_user, realm=realm, support_session=True
|
||||
)
|
||||
success_message = billing_session.process_support_view_request(support_view_request)
|
||||
context["success_message"] = success_message
|
||||
|
||||
if query:
|
||||
key_words = get_invitee_emails_set(query)
|
||||
|
||||
@@ -277,7 +299,7 @@ def support(
|
||||
for key_word in key_words:
|
||||
try:
|
||||
URLValidator()(key_word)
|
||||
parse_result = urlsplit(key_word)
|
||||
parse_result = urllib.parse.urlparse(key_word)
|
||||
hostname = parse_result.hostname
|
||||
assert hostname is not None
|
||||
if parse_result.port:
|
||||
@@ -341,9 +363,22 @@ def support(
|
||||
)
|
||||
plan_data: Dict[int, PlanData] = {}
|
||||
for realm in all_realms:
|
||||
billing_session = RealmBillingSession(user=None, realm=realm)
|
||||
realm_plan_data = get_current_plan_data_for_support_view(billing_session)
|
||||
plan_data[realm.id] = realm_plan_data
|
||||
current_plan = get_current_plan_by_realm(realm)
|
||||
plan_data[realm.id] = PlanData(
|
||||
customer=get_customer_by_realm(realm),
|
||||
current_plan=current_plan,
|
||||
)
|
||||
if current_plan is not None:
|
||||
new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(
|
||||
current_plan, timezone_now()
|
||||
)
|
||||
if last_ledger_entry is not None:
|
||||
if new_plan is not None:
|
||||
plan_data[realm.id].current_plan = new_plan
|
||||
else:
|
||||
plan_data[realm.id].current_plan = current_plan
|
||||
plan_data[realm.id].licenses = last_ledger_entry.licenses
|
||||
plan_data[realm.id].licenses_used = get_latest_seat_count(realm)
|
||||
context["plan_data"] = plan_data
|
||||
|
||||
def get_realm_owner_emails_as_string(realm: Realm) -> str:
|
||||
@@ -362,7 +397,7 @@ def support(
|
||||
|
||||
context["get_realm_owner_emails_as_string"] = get_realm_owner_emails_as_string
|
||||
context["get_realm_admin_emails_as_string"] = get_realm_admin_emails_as_string
|
||||
context["get_discount"] = get_customer_discount_for_support_view
|
||||
context["get_discount_for_realm"] = get_discount_for_realm
|
||||
context["get_org_type_display_name"] = get_org_type_display_name
|
||||
context["realm_icon_url"] = realm_icon_url
|
||||
context["Confirmation"] = Confirmation
|
||||
@@ -371,152 +406,3 @@ def support(
|
||||
)
|
||||
|
||||
return render(request, "analytics/support.html", context=context)
|
||||
|
||||
|
||||
def get_remote_servers_for_support(
|
||||
email_to_search: Optional[str], hostname_to_search: Optional[str]
|
||||
) -> List["RemoteZulipServer"]:
|
||||
if not email_to_search and not hostname_to_search:
|
||||
return []
|
||||
|
||||
remote_servers_query = RemoteZulipServer.objects.order_by("id").prefetch_related(
|
||||
"remoterealm_set"
|
||||
)
|
||||
if email_to_search:
|
||||
remote_servers_query = remote_servers_query.filter(contact_email__iexact=email_to_search)
|
||||
elif hostname_to_search:
|
||||
remote_servers_query = remote_servers_query.filter(hostname__icontains=hostname_to_search)
|
||||
|
||||
return list(remote_servers_query)
|
||||
|
||||
|
||||
@require_server_admin
|
||||
@has_request_variables
|
||||
def remote_servers_support(
|
||||
request: HttpRequest,
|
||||
query: Optional[str] = REQ("q", default=None),
|
||||
remote_server_id: Optional[int] = REQ(default=None, converter=to_non_negative_int),
|
||||
remote_realm_id: Optional[int] = REQ(default=None, converter=to_non_negative_int),
|
||||
discount: Optional[Decimal] = REQ(default=None, converter=to_decimal),
|
||||
sponsorship_pending: Optional[bool] = REQ(default=None, json_validator=check_bool),
|
||||
approve_sponsorship: bool = REQ(default=False, json_validator=check_bool),
|
||||
billing_modality: Optional[str] = REQ(
|
||||
default=None, str_validator=check_string_in(VALID_BILLING_MODALITY_VALUES)
|
||||
),
|
||||
modify_plan: Optional[str] = REQ(
|
||||
default=None, str_validator=check_string_in(VALID_MODIFY_PLAN_METHODS)
|
||||
),
|
||||
) -> HttpResponse:
|
||||
context: Dict[str, Any] = {}
|
||||
|
||||
if "success_message" in request.session:
|
||||
context["success_message"] = request.session["success_message"]
|
||||
del request.session["success_message"]
|
||||
|
||||
acting_user = request.user
|
||||
assert isinstance(acting_user, UserProfile)
|
||||
if settings.BILLING_ENABLED and request.method == "POST":
|
||||
# We check that request.POST only has two keys in it:
|
||||
# either the remote_server_id or a remote_realm_id,
|
||||
# and a field to change.
|
||||
keys = set(request.POST.keys())
|
||||
if "csrfmiddlewaretoken" in keys:
|
||||
keys.remove("csrfmiddlewaretoken")
|
||||
if len(keys) != 2:
|
||||
raise JsonableError(_("Invalid parameters"))
|
||||
|
||||
if remote_realm_id is not None:
|
||||
remote_realm_support_request = True
|
||||
remote_realm = RemoteRealm.objects.get(id=remote_realm_id)
|
||||
else:
|
||||
assert remote_server_id is not None
|
||||
remote_realm_support_request = False
|
||||
remote_server = RemoteZulipServer.objects.get(id=remote_server_id)
|
||||
|
||||
support_view_request = None
|
||||
|
||||
if approve_sponsorship:
|
||||
support_view_request = SupportViewRequest(support_type=SupportType.approve_sponsorship)
|
||||
elif sponsorship_pending is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.update_sponsorship_status,
|
||||
sponsorship_status=sponsorship_pending,
|
||||
)
|
||||
elif discount is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.attach_discount,
|
||||
discount=discount,
|
||||
)
|
||||
elif billing_modality is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.update_billing_modality,
|
||||
billing_modality=billing_modality,
|
||||
)
|
||||
elif modify_plan is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.modify_plan,
|
||||
plan_modification=modify_plan,
|
||||
)
|
||||
if support_view_request is not None:
|
||||
if remote_realm_support_request:
|
||||
success_message = RemoteRealmBillingSession(
|
||||
support_staff=acting_user, remote_realm=remote_realm
|
||||
).process_support_view_request(support_view_request)
|
||||
else:
|
||||
success_message = RemoteServerBillingSession(
|
||||
support_staff=acting_user, remote_server=remote_server
|
||||
).process_support_view_request(support_view_request)
|
||||
context["success_message"] = success_message
|
||||
|
||||
email_to_search = None
|
||||
hostname_to_search = None
|
||||
if query:
|
||||
if "@" in query:
|
||||
email_to_search = query
|
||||
else:
|
||||
hostname_to_search = query
|
||||
|
||||
remote_servers = get_remote_servers_for_support(
|
||||
email_to_search=email_to_search, hostname_to_search=hostname_to_search
|
||||
)
|
||||
remote_server_to_max_monthly_messages: Dict[int, Union[int, str]] = dict()
|
||||
server_support_data: Dict[int, SupportData] = {}
|
||||
realm_support_data: Dict[int, SupportData] = {}
|
||||
remote_realms: Dict[int, List[RemoteRealm]] = {}
|
||||
for remote_server in remote_servers:
|
||||
# Get remote realms attached to remote server
|
||||
remote_realms_for_server = list(
|
||||
remote_server.remoterealm_set.exclude(is_system_bot_realm=True)
|
||||
)
|
||||
remote_realms[remote_server.id] = remote_realms_for_server
|
||||
# Get plan data for remote realms
|
||||
for remote_realm in remote_realms[remote_server.id]:
|
||||
realm_billing_session = RemoteRealmBillingSession(remote_realm=remote_realm)
|
||||
remote_realm_data = get_data_for_support_view(realm_billing_session)
|
||||
realm_support_data[remote_realm.id] = remote_realm_data
|
||||
# Get plan data for remote server
|
||||
server_billing_session = RemoteServerBillingSession(remote_server=remote_server)
|
||||
remote_server_data = get_data_for_support_view(server_billing_session)
|
||||
server_support_data[remote_server.id] = remote_server_data
|
||||
# Get max monthly messages
|
||||
try:
|
||||
remote_server_to_max_monthly_messages[remote_server.id] = compute_max_monthly_messages(
|
||||
remote_server
|
||||
)
|
||||
except MissingDataError:
|
||||
remote_server_to_max_monthly_messages[remote_server.id] = "Recent data missing"
|
||||
|
||||
context["remote_servers"] = remote_servers
|
||||
context["remote_servers_support_data"] = server_support_data
|
||||
context["remote_server_to_max_monthly_messages"] = remote_server_to_max_monthly_messages
|
||||
context["remote_realms"] = remote_realms
|
||||
context["remote_realms_support_data"] = realm_support_data
|
||||
context["get_plan_type_name"] = get_plan_type_string
|
||||
context["get_org_type_display_name"] = get_org_type_display_name
|
||||
context["SPONSORED_PLAN_TYPE"] = RemoteZulipServer.PLAN_TYPE_COMMUNITY
|
||||
|
||||
return render(
|
||||
request,
|
||||
"analytics/remote_server_support.html",
|
||||
context=context,
|
||||
)
|
||||
|
||||
@@ -11,8 +11,7 @@ from analytics.views.activity_common import (
|
||||
make_table,
|
||||
)
|
||||
from zerver.decorator import require_server_admin
|
||||
from zerver.models import UserActivity, UserProfile
|
||||
from zerver.models.users import get_user_profile_by_id
|
||||
from zerver.models import UserActivity, UserProfile, get_user_profile_by_id
|
||||
|
||||
if settings.BILLING_ENABLED:
|
||||
pass
|
||||
|
||||
@@ -18,494 +18,22 @@ clients should check the `zulip_feature_level` field, present in the
|
||||
/register`](/api/register-queue) responses, to determine the API
|
||||
format used by the Zulip server that they are interacting with.
|
||||
|
||||
## Changes in Zulip 8.0
|
||||
## Changes in Zulip 7.5
|
||||
|
||||
**Feature level 237**
|
||||
**Feature level 186**
|
||||
|
||||
No changes; feature level used for Zulip 8.0 release.
|
||||
|
||||
**Feature level 236**
|
||||
|
||||
* [`POST /messages`](/api/send-message), [`POST
|
||||
/scheduled_messages`](/api/create-scheduled-message): The new
|
||||
`read_by_sender` parameter lets the client override the heuristic
|
||||
that determines whether the new message will be initially marked
|
||||
read by its sender.
|
||||
|
||||
**Feature level 235**
|
||||
|
||||
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults),
|
||||
[`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings):
|
||||
Added a new user setting, `automatically_follow_topics_where_mentioned`,
|
||||
that allows the user to automatically follow topics where the user is mentioned.
|
||||
|
||||
**Feature level 234**
|
||||
|
||||
* Mobile push notifications now include a `realm_name` field.
|
||||
* [`POST /mobile_push/test_notification`](/api/test-notify) now sends
|
||||
a test notification with `test` rather than `test-by-device-token`
|
||||
in the `event` field.
|
||||
|
||||
**Feature level 233**
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events):
|
||||
Renamed the event type `hotspots` and the `hotspots` array field in it
|
||||
to `onboarding_steps` as this event is sent to clients with remaining
|
||||
onboarding steps data that includes hotspots and one-time notices to display.
|
||||
Earlier, we had hotspots only. Added a `type` field to the objects in
|
||||
the renamed `onboarding_steps` array to distinguish between the two type
|
||||
of onboarding steps.
|
||||
|
||||
* `POST /users/me/onboarding_steps`: Added a new endpoint that
|
||||
deprecates the `/users/me/hotspots` endpoint. Added support for
|
||||
displaying one-time notices in addition to existing hotspots.
|
||||
This is now used as a common endpoint to mark both types of
|
||||
onboarding steps, i.e., 'hotspot' and 'one_time_notice'.
|
||||
There is no compatibility support for `/users/me/hotspots` as
|
||||
no client other than web app has this feature currently.
|
||||
|
||||
**Feature level 232**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added a new
|
||||
`user_list_incomplete` [client
|
||||
capability](/api/register-queue#parameter-client_capabilities)
|
||||
controlling whether `realm_users` contains "Unknown user"
|
||||
placeholder objects for users that the current user cannot access
|
||||
due to a `can_access_all_users_group` policy.
|
||||
|
||||
* [`GET /events`](/api/get-events): The new `user_list_incomplete`
|
||||
[client
|
||||
capability](/api/register-queue#parameter-client_capabilities)
|
||||
controls whether to send `realm_user` events with `op: "add"`
|
||||
containing "Unknown user" placeholder objects to clients when a new
|
||||
user is created that the client does not have access to due to a
|
||||
`can_access_all_users_group` policy.
|
||||
|
||||
**Feature level 231**
|
||||
|
||||
* [`POST /register`](/api/register-queue):
|
||||
`realm_push_notifications_enabled` now represents more accurately
|
||||
whether push notifications are actually enabled via the mobile push
|
||||
notifications service. Added
|
||||
`realm_push_notifications_enabled_end_timestamp` field to realm
|
||||
data.
|
||||
|
||||
* [`GET /events`](/api/get-events): A `realm` update event is now sent
|
||||
whenever `push_notifications_enabled` or
|
||||
`push_notifications_enabled_end_timestamp` changes.
|
||||
|
||||
**Feature level 230**
|
||||
|
||||
* [`GET /events`](/api/get-events): Added `has_trigger` field in
|
||||
hotspots events to identify if a hotspot will activate only when
|
||||
some specific event occurs.
|
||||
|
||||
**Feature level 229**
|
||||
|
||||
* [`PATCH /messages/{message_id}`](/api/update-message), [`POST
|
||||
/messages`](/api/send-message): Topic wildcard mentions involving
|
||||
large numbers of participants are now restricted by
|
||||
`wildcard_mention_policy`. The server now uses the
|
||||
`STREAM_WILDCARD_MENTION_NOT_ALLOWED` and
|
||||
`TOPIC_WILDCARD_MENTION_NOT_ALLOWED` error codes when a message is
|
||||
rejected because of `wildcard_mention_policy`.
|
||||
|
||||
**Feature level 228**
|
||||
|
||||
* [`GET /events`](/api/get-events): `realm_user` events with `op: "update"`
|
||||
are now only sent to users who can access the modified user.
|
||||
|
||||
* [`GET /events`](/api/get-events): `presence` events are now only sent to
|
||||
users who can access the user who comes back online if the
|
||||
`CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE` server setting is set
|
||||
to `true`.
|
||||
|
||||
* [`GET /events`](/api/get-events): `user_status` events are now only
|
||||
sent to users who can access the modified user.
|
||||
|
||||
* [`GET /realm/presence`](/api/get-presence): The endpoint now returns
|
||||
presence information of accessible users only if the
|
||||
`CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE` server setting is set
|
||||
to `true`.
|
||||
|
||||
* [`GET /events`](/api/get-events): `realm_user` events with `op: "add"`
|
||||
are now also sent when a guest user gains access to a user.
|
||||
|
||||
* [`GET /events`](/api/get-events): `realm_user` events with `op: "remove"`
|
||||
are now also sent when a guest user loses access to a user.
|
||||
|
||||
**Feature level 227**
|
||||
|
||||
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults),
|
||||
[`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings):
|
||||
Added `DMs, mentions, and followed topics` option for `desktop_icon_count_display`
|
||||
setting, and renumbered the options.
|
||||
The total unread count of DMs, mentions, and followed topics appears in
|
||||
desktop sidebar and browser tab when this option is configured.
|
||||
|
||||
**Feature level 226**
|
||||
* [`POST /register`](/api/register-queue): `streams` field in the response
|
||||
now included web-public streams as well. This change was backported from
|
||||
Zulip 8.0, where it was introduced in feature level 205.
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events),
|
||||
[`GET /users/me/subscriptions`](/api/get-subscriptions): Removed
|
||||
`email_address` field from subscription objects.
|
||||
`email_address` field from subscription objects. This change was backported
|
||||
from Zulip 8.0, where it was introduced in feature level 226.
|
||||
|
||||
* [`GET /streams/{stream_id}/email_address`](/api/get-stream-email-address):
|
||||
Added new endpoint to get email address of a stream.
|
||||
|
||||
**Feature level 225**
|
||||
|
||||
* `PATCH /realm`, [`POST /register`](/api/register-queue),
|
||||
[`GET /events`](/api/get-events): Added `can_access_all_users_group_id`
|
||||
realm setting, which is the ID of the user group whose members can
|
||||
access all the users in the oragnization.
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added `allowed_system_groups`
|
||||
field to configuration data object of permission settings passed in
|
||||
`server_supported_permission_settings`.
|
||||
|
||||
**Feature level 224**
|
||||
|
||||
* [`GET /events`](/api/get-events), [`GET /messages`](/api/get-messages),
|
||||
[`GET /messages/{message_id}`](/api/get-message): The `wildcard_mentioned`
|
||||
flag was deprecated, replaced with `stream_wildcard_mentioned` and
|
||||
`topic_wildcard_mentioned`, but it is still available for backwards compatibility.
|
||||
|
||||
**Feature level 223**
|
||||
|
||||
* `POST /users/me/apns_device_token`:
|
||||
The `appid` parameter is now required.
|
||||
Previously it defaulted to the server setting `ZULIP_IOS_APP_ID`,
|
||||
defaulting to "org.zulip.Zulip".
|
||||
|
||||
* `POST /remotes/server/register`: The `ios_app_id` parameter is now
|
||||
required when `kind` is 1, i.e. when registering an APNs token.
|
||||
Previously it was ignored, and the push bouncer effectively
|
||||
assumed its value was the server setting `APNS_TOPIC`,
|
||||
defaulting to "org.zulip.Zulip".
|
||||
|
||||
**Feature level 222**
|
||||
|
||||
* [`GET /events`](/api/get-events): When a user is deactivated or
|
||||
reactivated, the server uses `realm_user` events with `op: "update"`
|
||||
updating the `is_active` field, instead of `realm_user` events with
|
||||
`op: "remove"` and `op: "add"`, respectively.
|
||||
|
||||
* [`GET /events`](/api/get-events): When a bot is deactivated or
|
||||
reactivated, the server sends `realm_bot` events with `op: "update"`
|
||||
updating the `is_active` field, instead of `realm_bot` events with
|
||||
`op: "remove"` and `op: "add"`, respectively.
|
||||
|
||||
**Feature level 221**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added `server_supported_permission_settings`
|
||||
field in the response which contains configuration data for various permission
|
||||
settings.
|
||||
|
||||
**Feature level 220**
|
||||
|
||||
* [`GET /events`](/api/get-events): Stream creation events for web-public
|
||||
streams are now sent to all guest users in the organization as well.
|
||||
|
||||
* [`GET /events`](/api/get-events): The `subscription` events for `op:
|
||||
"peer_add"` and `op: "peer_remove"` are now sent to subscribed guest
|
||||
users for public streams and to all the guest users for web-public
|
||||
streams; previously, they incorrectly only received these for
|
||||
private streams.
|
||||
|
||||
**Feature level 219**
|
||||
|
||||
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults)
|
||||
[`POST /register`](/api/register-queue), [`GET /events`](/api/get-events),
|
||||
[`PATCH /settings`](/api/update-settings): Renamed `default_view` and
|
||||
`escape_navigates_to_default_view` settings to `web_home_view` and
|
||||
`web_escape_navigates_to_home_view` respectively.
|
||||
* [`POST /user_topics`](/api/update-user-topic), [`POST
|
||||
register`](/api/register-queue), [`GET /events`](/api/get-events):
|
||||
Added followed as a supported value for visibility policies in
|
||||
`user_topic` objects.
|
||||
|
||||
**Feature level 218**
|
||||
|
||||
* [`POST /messages`](/api/send-message): Added an optional
|
||||
`automatic_new_visibility_policy` enum field in the success response
|
||||
to indicate the new visibility policy value due to the [visibility policy settings](/help/mute-a-topic)
|
||||
during the send message action.
|
||||
|
||||
**Feature level 217**
|
||||
|
||||
* [`POST /mobile_push/test_notification`](/api/test-notify): Added new endpoint
|
||||
to send a test push notification to a mobile device or devices.
|
||||
|
||||
**Feature level 216**:
|
||||
|
||||
* `PATCH /realm`, [`POST register`](/api/register-queue),
|
||||
[`GET /events`](/api/get-events): Added `enable_guest_user_indicator`
|
||||
setting to control whether "(guest)" is added to user names in UI.
|
||||
|
||||
**Feature level 215**
|
||||
|
||||
* [`GET /events`](/api/get-events): Replaced the value `private`
|
||||
with `direct` in the `message_type` field for the `typing` events
|
||||
sent when a user starts or stops typing a message.
|
||||
|
||||
* [`POST /typing`](/api/set-typing-status): Stopped supporting `private`
|
||||
as a valid value for the `type` parameter.
|
||||
|
||||
* [`POST /typing`](/api/set-typing-status): Stopped using the `to` parameter
|
||||
for the `"stream"` type. Previously, in the case of the `"stream"` type, it
|
||||
accepted a single-element list containing the ID of the stream. Added an
|
||||
optional parameter, `stream_id`. Now, `to` is used only for `"direct"` type.
|
||||
In the case of `"stream"` type, `stream_id` and `topic` are used.
|
||||
|
||||
* Note that stream typing notifications were not enabled in any Zulip client
|
||||
prior to feature level 215.
|
||||
|
||||
**Feature level 214**
|
||||
|
||||
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults),
|
||||
[`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings):
|
||||
Added two new user settings, `automatically_follow_topics_policy` and
|
||||
`automatically_unmute_topics_in_muted_streams_policy`. The settings control the
|
||||
user's preference on which topics the user will automatically 'follow' and
|
||||
'unmute in muted streams' respectively.
|
||||
|
||||
**Feature level 213**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Fixed incorrect handling of
|
||||
unmuted and followed topics in calculating the `mentions` and
|
||||
`count` fields of the `unread_msgs` object.
|
||||
|
||||
**Feature level 212**
|
||||
|
||||
* [`GET /events`](/api/get-events), [`POST /register`](/api/register-queue),
|
||||
`PATCH /realm`: Added the `jitsi_server_url` field to the `realm` object,
|
||||
allowing organizations to set a custom Jitsi Meet server. Previously, this
|
||||
was only available as a server-level configuration.
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added `server_jitsi_server_url`
|
||||
fields to the `realm` object. The existing `jitsi_server_url` will now be
|
||||
calculated as `realm_jitsi_server_url || server_jitsi_server_url`.
|
||||
|
||||
**Feature level 211**
|
||||
|
||||
* [`POST /streams/{stream_id}/delete_topic`](/api/delete-topic),
|
||||
[`POST /mark_all_as_read`](/api/mark-all-as-read):
|
||||
Added a `complete` boolean field in the success response to indicate
|
||||
whether all or only some of the targeted messages were processed.
|
||||
This replaces the use of `"result": "partially_completed"` (introduced
|
||||
in feature levels 154 and 153), so that these endpoints now send a
|
||||
`result` string of either `"success"` or `"error"`, like the rest of
|
||||
the Zulip API.
|
||||
|
||||
**Feature level 210**
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings),
|
||||
[`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults):
|
||||
Added new `web_stream_unreads_count_display_policy` display setting, which controls in
|
||||
which streams (all/unmuted/none) unread messages count shows up
|
||||
in left sidebar.
|
||||
|
||||
**Feature level 209**
|
||||
|
||||
* `PATCH /realm`, [`POST /register`](/api/register-queue),
|
||||
[`GET /events`](/api/get-events): Added `create_multiuse_invite_group`
|
||||
realm setting, which is the ID of the user group whose members can
|
||||
create [reusable invitation links](/help/invite-new-users#create-a-reusable-invitation-link)
|
||||
to an organization. Previously, only admin users could create these
|
||||
links.
|
||||
|
||||
* `POST /invites/multiuse`: Non-admin users can now use this endpoint
|
||||
to create reusable invitation links. Previously, this endpoint was
|
||||
restricted to admin users only.
|
||||
|
||||
* `GET /invites`: Endpoint response for non-admin users now includes both
|
||||
email invitations and reusable invitation links that they have created.
|
||||
Previously, non-admin users could only create email invitations, and
|
||||
therefore the response did not include reusable invitation links for these users.
|
||||
|
||||
* `DELETE /invites/multiuse/{invite_id}`: Non-admin users can now revoke
|
||||
reusable invitation links they have created. Previously, only admin users could
|
||||
create and revoke reusable invitation links.
|
||||
|
||||
* [`GET /events`](/api/get-events): When the set of invitations in an
|
||||
organization changes, an `invites_changed` event is now sent to the
|
||||
creator of the changed invitation, as well as all admin users.
|
||||
Previously, this event was only sent to admin users.
|
||||
|
||||
**Feature level 208**
|
||||
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe),
|
||||
[`DELETE /users/me/subscriptions`](/api/unsubscribe): These endpoints
|
||||
now return an HTTP status code of 400 with `code: "BAD_REQUEST"` in
|
||||
the error response when a user specified in the `principals` parameter
|
||||
is deactivated or does not exist. Previously, these endpoints returned
|
||||
an HTTP status code of 403 with `code: "UNAUTHORIZED_PRINCIPAL"` in the
|
||||
error response for these cases.
|
||||
|
||||
**Feature level 207**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added `display_name` and
|
||||
`all_event_types` fields to the `realm_incoming_webhook_bots` object.
|
||||
|
||||
**Feature level 206**
|
||||
|
||||
* `POST /calls/zoom/create`: Added `is_video_call` parameter
|
||||
controlling whether to request a Zoom meeting that defaults to
|
||||
having video enabled.
|
||||
|
||||
**Feature level 205**
|
||||
|
||||
* [`POST /register`](/api/register-queue): `streams` field in the response
|
||||
now includes [web-public streams](/help/public-access-option) as well.
|
||||
* [`GET /events`](/api/get-events): Events for stream creation and deletion
|
||||
are now sent to clients when a user gains or loses access to any streams
|
||||
due to a change in their [role](/help/roles-and-permissions).
|
||||
* [`GET /events`](/api/get-events): The `subscription` events for `op:
|
||||
"peer_add"` are now sent to clients when a user gains access to a stream
|
||||
due to a change in their role.
|
||||
|
||||
**Feature level 204**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added
|
||||
`server_typing_started_wait_period_milliseconds`,
|
||||
`server_typing_stopped_wait_period_milliseconds`, and
|
||||
`server_typing_started_expiry_period_milliseconds` fields
|
||||
for clients to use when implementing [typing
|
||||
notifications](/api/set-typing-status) protocol.
|
||||
|
||||
**Feature level 203**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Add
|
||||
`realm_date_created` field to realm data.
|
||||
|
||||
**Feature level 202**
|
||||
|
||||
* [`PATCH /realm/linkifiers`](/api/reorder-linkifiers): Added new endpoint
|
||||
to support changing the order in which linkifiers will be processed.
|
||||
|
||||
**Feature level 201**
|
||||
|
||||
* `POST /zulip-outgoing-webhook`: Renamed the notification trigger
|
||||
`private_message` to `direct_message`.
|
||||
|
||||
**Feature level 200**
|
||||
|
||||
* [`PATCH /streams/{stream_id}`](/api/update-stream): Added
|
||||
`is_default_stream` parameter to change whether the stream is a
|
||||
default stream for new users in the organization.
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe): Added
|
||||
`is_default_stream` parameter which determines whether any streams
|
||||
created by this request will be default streams for new users.
|
||||
|
||||
**Feature level 199**
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events),
|
||||
[`GET /streams`](/api/get-streams),
|
||||
[`GET /streams/{stream_id}`](/api/get-stream-by-id): Stream objects now
|
||||
include a `stream_weekly_traffic` field indicating the stream's level of
|
||||
traffic.
|
||||
|
||||
**Feature level 198**
|
||||
|
||||
* [`GET /events`](/api/get-events), [`POST /register`](/api/register-queue),
|
||||
[`GET /user_groups`](/api/get-user-groups),
|
||||
[`POST /user_groups/create`](/api/create-user-group),
|
||||
[`PATCH /user_groups/{user_group_id}`](/api/update-user-group):Renamed
|
||||
group setting `can_mention_group_id` to `can_mention_group`.
|
||||
|
||||
**Feature level 197**
|
||||
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe),
|
||||
[`PATCH /streams/{stream_id}`](/api/update-stream),
|
||||
[`GET /users/me/subscriptions`](/api/get-subscriptions),
|
||||
[`GET /streams`](/api/get-streams),
|
||||
[`POST /register`](/api/register-queue),
|
||||
[`GET /events`](/api/get-events): Renamed
|
||||
stream setting `can_remove_subscribers_group_id`
|
||||
to `can_remove_subscribers_group`.
|
||||
|
||||
**Feature level 196**
|
||||
|
||||
* [`POST /realm/playgrounds`](/api/add-code-playground): `url_prefix` is
|
||||
replaced by `url_template`, which only accepts [RFC 6570][rfc6570] compliant
|
||||
URL templates. The old prefix format is no longer supported.
|
||||
* [`GET /events`](/api/get-events), [`POST /register`](/api/register-queue):
|
||||
`url_prefix` is replaced by `url_template` in `realm_playgrounds` events.
|
||||
|
||||
**Feature level 195**
|
||||
|
||||
* [`GET /events`](/api/get-events), [`POST /register`](/api/register-queue):
|
||||
The `default_code_block_language` realm setting is now consistently an
|
||||
empty string when no default pygments language code is set. Previously,
|
||||
the server had a bug that meant it might represent no default for this
|
||||
realm setting as either `null` or an empty string. Clients supporting
|
||||
older server versions should treat either value (`null` or `""`) as no
|
||||
default being set.
|
||||
|
||||
**Feature level 194**
|
||||
|
||||
* [`GET /messages`](/api/get-messages),
|
||||
[`GET /messages/matches_narrow`](/api/check-messages-match-narrow),
|
||||
[`POST /message/flags/narrow`](/api/update-message-flags-for-narrow),
|
||||
[`POST /register`](/api/register-queue):
|
||||
For [search/narrow filters](/api/construct-narrow) with the `id`
|
||||
operator, added support for encoding the message ID operand as either
|
||||
a string or an integer. Previously, only string encoding was supported.
|
||||
|
||||
**Feature level 193**
|
||||
|
||||
* [`POST /messages/{message_id}/reactions`](/api/add-reaction),
|
||||
[`DELETE /messages/{message_id}/reactions`](/api/remove-reaction):
|
||||
Endpoints return specific error responses if an emoji reaction
|
||||
already exists when adding a reaction (`"code": "REACTION_ALREADY_EXISTS"`)
|
||||
or if an emoji reaction does not exist when deleting a reaction
|
||||
(`"code": "REACTION_DOES_NOT_EXIST"`). Previously, these errors
|
||||
returned the `"BAD_REQUEST"` code.
|
||||
|
||||
**Feature level 192**
|
||||
|
||||
* [`GET /events`](/api/get-events): Stream creation events are now
|
||||
sent when guest users gain access to a public stream by being
|
||||
subscribed. Guest users previously only received these events when
|
||||
subscribed to private streams.
|
||||
|
||||
**Feature level 191**
|
||||
|
||||
* [`GET /events`](/api/get-events), [`POST /register`](/api/register-queue),
|
||||
[`GET /user_groups`](/api/get-user-groups): Add `can_mention_group_id` to
|
||||
user group objects.
|
||||
* [`POST /user_groups/create`](/api/create-user-group): Added `can_mention_group_id`
|
||||
parameter to support setting the user group whose members can mention the new user
|
||||
group.
|
||||
* [`PATCH /user_groups/{user_group_id}`](/api/update-user-group): Added
|
||||
`can_mention_group_id` parameter to support changing the user group whose
|
||||
members can mention the specified user group.
|
||||
|
||||
**Feature level 190**
|
||||
|
||||
* [`DELETE /realm/emoji/{emoji_name}`](/api/deactivate-custom-emoji): This endpoint
|
||||
now returns an HTTP status code of 404 when an emoji does not exist, instead of 400.
|
||||
|
||||
**Feature level 189**
|
||||
|
||||
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults),
|
||||
[`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings):
|
||||
Added new boolean user settings `enable_followed_topic_email_notifications`,
|
||||
`enable_followed_topic_push_notifications`,
|
||||
`enable_followed_topic_wildcard_mentions_notify`,
|
||||
`enable_followed_topic_desktop_notifications`
|
||||
and `enable_followed_topic_audible_notifications` to control whether a user
|
||||
receives email, push, wildcard mention, visual desktop and audible desktop
|
||||
notifications, respectively, for messages sent to followed topics.
|
||||
|
||||
**Feature level 188**
|
||||
|
||||
* [`POST /users/me/muted_users/{muted_user_id}`](/api/mute-user),
|
||||
[`DELETE /users/me/muted_users/{muted_user_id}`](/api/unmute-user):
|
||||
Added support to mute/unmute bot users.
|
||||
|
||||
Feature levels 186-187 are reserved for future use in 7.x maintenance
|
||||
releases.
|
||||
Added new endpoint to get email address of a stream. This change was
|
||||
backported from Zulip 8.0, where it was introduced in feature level 226.
|
||||
|
||||
## Changes in Zulip 7.0
|
||||
|
||||
@@ -569,7 +97,7 @@ No changes; feature level used for Zulip 7.0 release.
|
||||
|
||||
**Feature level 178**
|
||||
|
||||
* `POST /users/me/presence`,
|
||||
* `POST users/me/presence`,
|
||||
[`GET /users/<user_id_or_email>/presence`](/api/get-user-presence),
|
||||
[`GET /realm/presence`](/api/get-presence),
|
||||
[`POST /register`](/api/register-queue),
|
||||
@@ -595,7 +123,7 @@ No changes; feature level used for Zulip 7.0 release.
|
||||
**Feature level 176**
|
||||
|
||||
* [`POST /realm/filters`](/api/add-linkifier),
|
||||
[`PATCH /realm/filters/<int:filter_id>`](/api/update-linkifier):
|
||||
[`PATCH realm/filters/<int:filter_id>`](/api/update-linkifier):
|
||||
The `url_format_string` parameter is replaced by `url_template`.
|
||||
[Linkifiers](/help/add-a-custom-linkifier) now only accept
|
||||
[RFC 6570][rfc6570] compliant URL templates. The old URL format
|
||||
@@ -815,29 +343,26 @@ No changes; feature level used for Zulip 6.0 release.
|
||||
**Feature level 154**
|
||||
|
||||
* [`POST /streams/{stream_id}/delete_topic`](/api/delete-topic):
|
||||
When the process of deleting messages times out, but successfully
|
||||
deletes some messages in the topic (see feature level 147 for when
|
||||
this endpoint started deleting messages in batches), a success
|
||||
response with `"result": "partially_completed"` will now be returned
|
||||
by the server, analogically to the `POST /mark_all_as_read` endpoint
|
||||
(see feature level 153 entry below).
|
||||
When the process of deleting messages times out, a success response
|
||||
with "partially_completed" result will now be returned by the server,
|
||||
analogically to the `/mark_all_as_read` endpoint.
|
||||
|
||||
**Feature level 153**
|
||||
|
||||
* [`POST /mark_all_as_read`](/api/mark-all-as-read): Messages are now
|
||||
marked as read in batches, so that progress will be made even if the
|
||||
request times out because of an extremely large number of unread
|
||||
messages to process. Upon timeout, a success response with
|
||||
`"result": "partially_completed"` will be returned by the server.
|
||||
messages to process. Upon timeout, a success response with a
|
||||
"partially_completed" result will be returned by the server.
|
||||
|
||||
**Feature level 152**
|
||||
|
||||
* [`PATCH /messages/{message_id}`](/api/update-message):
|
||||
The default value for `send_notification_to_old_thread` was changed from
|
||||
`true` to `false`.
|
||||
When moving a topic within a stream, the `send_notification_to_old_thread`
|
||||
and `send_notification_to_new_thread` parameters are now respected, and by
|
||||
default a notification is sent to the new thread.
|
||||
* [`PATCH /messages/{message_id}`](/api/update-message): The
|
||||
`send_notification_to_old_thread` and
|
||||
`send_notification_to_new_thread` parameters are now respected when
|
||||
moving a topic within a stream. The default value for
|
||||
`send_notification_to_old_thread` was changed from `true` to
|
||||
`false`.
|
||||
|
||||
**Feature level 151**
|
||||
|
||||
@@ -890,7 +415,7 @@ user's profile.
|
||||
|
||||
**Feature level 145**
|
||||
|
||||
* [`DELETE /users/me/subscriptions`](/api/unsubscribe): Normal users can
|
||||
* [`DELETE users/me/subscriptions`](/api/unsubscribe): Normal users can
|
||||
now remove bots that they own from streams.
|
||||
|
||||
**Feature level 144**
|
||||
@@ -911,7 +436,7 @@ user's profile.
|
||||
|
||||
**Feature level 142**
|
||||
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions), [`GET
|
||||
* [`GET users/me/subscriptions`](/api/get-subscriptions), [`GET
|
||||
/streams`](/api/get-streams), [`POST /register`](/api/register-queue),
|
||||
[`GET /events`](/api/get-events): Added `can_remove_subscribers_group_id`
|
||||
field to Stream and Subscription objects.
|
||||
@@ -982,9 +507,6 @@ user's profile.
|
||||
to the response. This generalizes and replaces the previous
|
||||
`muted_topics` array, which will no longer be sent if `user_topic`
|
||||
is included in `fetch_event_types`.
|
||||
* [`GET /events`](/api/get-events): When private streams are made
|
||||
public, `stream` events for `op: "create"` and `subscription` events
|
||||
for `op: "peer_add"` are now sent to clients.
|
||||
|
||||
**Feature level 133**
|
||||
|
||||
@@ -1081,12 +603,11 @@ No changes; feature level used for Zulip 5.0 release.
|
||||
|
||||
**Feature level 119**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added `other_user_id` field
|
||||
to the `pms` objects in the `unread_msgs` data set, deprecating the
|
||||
less clearly named `sender_id` field. This change was motivated by
|
||||
the possibility that a one-on-one direct message sent by the current
|
||||
user to another user could be marked as unread. The `sender_id` field
|
||||
is still present for backwards compatibility with older server versions.
|
||||
* [`POST /register`](/api/register-queue): The `unread_msgs` section
|
||||
of the response now prefers `other_user_id` over the poorly named
|
||||
`sender_id` field in the `pms` dictionaries. This change is
|
||||
motivated by the possibility that a message you yourself sent to
|
||||
another user could be marked as unread.
|
||||
|
||||
**Feature level 118**
|
||||
|
||||
@@ -1101,7 +622,7 @@ No changes; feature level used for Zulip 5.0 release.
|
||||
field. These changes substantially simplify client complexity for
|
||||
processing historical message edits.
|
||||
|
||||
* [`GET /messages/{message_id}/history`](/api/get-message-history):
|
||||
* [`GET messages/{message_id}/history`](/api/get-message-history):
|
||||
Added `stream` field to message history `snapshot` indicating
|
||||
the updated stream ID of messages moved to a new stream.
|
||||
|
||||
@@ -1177,7 +698,7 @@ No changes; feature level used for Zulip 5.0 release.
|
||||
* [`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings),
|
||||
[`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults):
|
||||
Added user setting `escape_navigates_to_default_view` to allow users to
|
||||
[disable the keyboard shortcut](/help/configure-home-view) for the `Esc` key that
|
||||
[disable the keyboard shortcut](/help/configure-default-view) for the `Esc` key that
|
||||
navigates the app to the default view.
|
||||
|
||||
**Feature level 106**
|
||||
@@ -1250,7 +771,7 @@ No changes; feature level used for Zulip 5.0 release.
|
||||
|
||||
**Feature level 98**
|
||||
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe): Added `is_web_public` parameter
|
||||
* [`POST /subscribe`](/api/subscribe): Added `is_web_public` parameter
|
||||
for requesting the creation of a web-public stream.
|
||||
* [`PATCH /streams/{stream_id}`](/api/update-stream): Added
|
||||
`is_web_public` parameter for converting a stream into a web-public stream.
|
||||
@@ -1390,7 +911,7 @@ No changes; feature level used for Zulip 5.0 release.
|
||||
|
||||
**Feature level 83**
|
||||
|
||||
* [`POST /register`](/api/register-queue): The `cross_realm_bots`
|
||||
* * [`POST /register`](/api/register-queue): The `cross_realm_bots`
|
||||
section of the response now uses the `is_system_bot` flag to
|
||||
indicate whether the bot is a system bot.
|
||||
|
||||
@@ -1563,7 +1084,7 @@ No changes; feature level used for Zulip 4.0 release.
|
||||
events to support typing notifications in stream messages. These new
|
||||
events are only sent to clients with `client_capabilities`
|
||||
showing support for `stream_typing_notifications`.
|
||||
* [`POST /typing`](/api/set-typing-status): Added support
|
||||
* [`POST /set-typing-status`](/api/set-typing-status): Added support
|
||||
for sending typing notifications for stream messages.
|
||||
|
||||
**Feature level 57**
|
||||
@@ -1643,8 +1164,6 @@ field with an integer field `invite_to_realm_policy`.
|
||||
* [`GET /events`](/api/get-events): Added new event type `muted_users`
|
||||
which will be sent to a user when the set of users muted by them has
|
||||
changed.
|
||||
* [`POST /register`](/api/register-queue): Added a new `muted_users` field,
|
||||
which identifies the set of other users the current user has muted.
|
||||
|
||||
**Feature level 47**
|
||||
|
||||
@@ -1679,7 +1198,7 @@ field with an integer field `invite_to_realm_policy`.
|
||||
**Feature level 42**
|
||||
|
||||
* `PATCH /settings/display`: Added a new `default_view` setting allowing
|
||||
the user to [set the default view](/help/configure-home-view).
|
||||
the user to [set the default view](/help/configure-default-view).
|
||||
|
||||
**Feature level 41**
|
||||
|
||||
@@ -1716,10 +1235,8 @@ field with an integer field `invite_to_realm_policy`.
|
||||
|
||||
**Feature level 35**
|
||||
|
||||
* [`GET /events`](/api/get-events): The `subscription` events for
|
||||
`peer_add` and `peer_remove` now include `user_ids` and `stream_ids`
|
||||
arrays. Previously, these events included singular `user_id` and
|
||||
`stream_id` integers.
|
||||
* The peer_add and peer_remove subscription events now have plural
|
||||
versions of `user_ids` and `stream_ids`.
|
||||
|
||||
**Feature level 34**
|
||||
|
||||
@@ -1742,7 +1259,7 @@ field with an integer field `invite_to_realm_policy`.
|
||||
|
||||
**Feature level 31**
|
||||
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions): Added a
|
||||
* [`GET users/me/subscriptions`](/api/get-subscriptions): Added a
|
||||
`role` field to Subscription objects representing whether the user
|
||||
is a stream administrator.
|
||||
|
||||
@@ -1755,7 +1272,7 @@ user to be a stream administrator at this feature level.
|
||||
|
||||
**Feature level 30**
|
||||
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions), [`GET
|
||||
* [`GET users/me/subscriptions`](/api/get-subscriptions), [`GET
|
||||
/streams`](/api/get-streams): Added `date_created` to Stream
|
||||
objects.
|
||||
* [`POST /users`](/api/create-user), `POST /bots`: The ID of the newly
|
||||
@@ -1773,9 +1290,8 @@ releases.
|
||||
|
||||
**Feature level 26**
|
||||
|
||||
* [`GET /messages`](/api/get-messages), [`GET /events`](/api/get-events):
|
||||
The `sender_short_name` field is no longer included in message objects
|
||||
returned by these endpoints.
|
||||
* [`GET /messages`](/api/get-messages): `sender_short_name` field is no
|
||||
longer included in return values for this endpoint.
|
||||
* [`GET /messages`](/api/get-messages) : Removed `short_name` field from
|
||||
`display_recipient` array objects.
|
||||
|
||||
@@ -1826,9 +1342,9 @@ No changes; feature level used for Zulip 3.0 release.
|
||||
|
||||
**Feature level 19**
|
||||
|
||||
* [`GET /events`](/api/get-events): The `subscription` events for
|
||||
`peer_add` and `peer_remove` now identify the modified
|
||||
stream by the `stream_id` field, replacing the old `name` field.
|
||||
* [`GET /events`](/api/get-events): `subscriptions` event with
|
||||
`op="peer_add"` and `op="peer_remove"` now identify the modified
|
||||
stream by a `stream_id` field, replacing the old `name` field.
|
||||
|
||||
**Feature level 18**
|
||||
|
||||
@@ -1837,11 +1353,11 @@ No changes; feature level used for Zulip 3.0 release.
|
||||
|
||||
**Feature level 17**
|
||||
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions),
|
||||
* [`GET users/me/subscriptions`](/api/get-subscriptions),
|
||||
[`GET /streams`](/api/get-streams): Added
|
||||
`message_retention_days` to Stream objects.
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe), [`PATCH
|
||||
/streams/{stream_id}`](/api/update-stream): Added `message_retention_days`
|
||||
* [`POST users/me/subscriptions`](/api/subscribe), [`PATCH
|
||||
streams/{stream_id}`](/api/update-stream): Added `message_retention_days`
|
||||
parameter.
|
||||
|
||||
**Feature level 16**
|
||||
@@ -1859,7 +1375,7 @@ No changes; feature level used for Zulip 3.0 release.
|
||||
|
||||
**Feature level 14**
|
||||
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions): Removed
|
||||
* [`GET users/me/subscriptions`](/api/get-subscriptions): Removed
|
||||
the `is_old_stream` field from Stream objects. This field was
|
||||
always equivalent to `stream_weekly_traffic != null` on the same object.
|
||||
|
||||
@@ -1876,7 +1392,7 @@ No changes; feature level used for Zulip 3.0 release.
|
||||
|
||||
**Feature level 12**
|
||||
|
||||
* [`GET /users/{user_id}/subscriptions/{stream_id}`](/api/get-subscription-status):
|
||||
* [`GET users/{user_id}/subscriptions/{stream_id}`](/api/get-subscription-status):
|
||||
New endpoint added for checking if another user is subscribed to a stream.
|
||||
|
||||
**Feature level 11**
|
||||
@@ -1886,24 +1402,22 @@ No changes; feature level used for Zulip 3.0 release.
|
||||
time limit before community topic editing is forbidden. A `null`
|
||||
value means no limit. This was previously hard-coded in the server
|
||||
as 86400 seconds (1 day).
|
||||
* [`POST /register`](/api/register-queue): The response now contains
|
||||
an `is_owner` boolean field, which is similar to the existing
|
||||
`is_admin` and `is_guest` fields.
|
||||
* [`POST /typing`](/api/set-typing-status): Removed legacy
|
||||
support for sending email addresses in the `to` parameter, rather
|
||||
than user IDs, to encode direct message recipients.
|
||||
* [`POST /register`](/api/register-queue): The response now contains a
|
||||
`is_owner`, similar to the existing `is_admin` and `is_guest` fields.
|
||||
* [`POST /set-typing-status`](/api/set-typing-status): Removed legacy support for sending email
|
||||
addresses, rather than user IDs, to encode private message recipients.
|
||||
|
||||
**Feature level 10**
|
||||
|
||||
* [`GET /users/me`](/api/get-own-user): Added `avatar_version`, `is_guest`,
|
||||
* [`GET users/me`](/api/get-own-user): Added `avatar_version`, `is_guest`,
|
||||
`is_active`, `timezone`, and `date_joined` fields to the User objects.
|
||||
* [`GET /users/me`](/api/get-own-user): Removed `client_id` and `short_name`
|
||||
* [`GET users/me`](/api/get-own-user): Removed `client_id` and `short_name`
|
||||
from the response to this endpoint. These fields had no purpose and
|
||||
were inconsistent with other API responses describing users.
|
||||
|
||||
**Feature level 9**
|
||||
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe), [`DELETE
|
||||
* [`POST users/me/subscriptions`](/api/subscribe), [`DELETE
|
||||
/users/me/subscriptions`](/api/unsubscribe): Other users to
|
||||
subscribe/unsubscribe, declared in the `principals` parameter, can
|
||||
now be referenced by user_id, rather than Zulip display email
|
||||
@@ -1992,7 +1506,7 @@ No changes; feature level used for Zulip 3.0 release.
|
||||
Added `prev_stream` as a potential property of the `edit_history` object
|
||||
within message objects to indicate when a message was moved to another
|
||||
stream.
|
||||
* [`GET /messages/{message_id}/history`](/api/get-message-history):
|
||||
* [`GET messages/{message_id}/history`](/api/get-message-history):
|
||||
`prev_stream` is present in `snapshot` objects within `message_history`
|
||||
object when a message was moved to another stream.
|
||||
* [`GET /server_settings`](/api/get-server-settings): Added
|
||||
@@ -2096,7 +1610,7 @@ No changes; feature level used for Zulip 3.0 release.
|
||||
encoding topics using the `topic` parameter name. The previous
|
||||
`subject` parameter name was deprecated but is still supported for
|
||||
backwards-compatibility.
|
||||
* [`POST /typing`](/api/set-typing-status): Added support for specifying the
|
||||
* [`POST /set-typing-status`](/api/set-typing-status): Added support for specifying the
|
||||
recipients with user IDs, deprecating the original API of specifying
|
||||
them using email addresses.
|
||||
|
||||
|
||||
@@ -65,33 +65,12 @@ filters did.
|
||||
|
||||
## Narrows that use IDs
|
||||
|
||||
### Message IDs
|
||||
|
||||
The `near` and `id` operators, documented in the help center, use message
|
||||
IDs for their operands.
|
||||
|
||||
* `near:12345`: Search messages around the message with ID `12345`.
|
||||
* `id:12345`: Search for only message with ID `12345`.
|
||||
|
||||
The message ID operand for the `id` operator may be encoded as either a
|
||||
number or a string. The message ID operand for the `near` operator must
|
||||
be encoded as a string.
|
||||
|
||||
**Changes**: Prior to Zulip 8.0 (feature level 194), the message ID
|
||||
operand for the `id` operator needed to be encoded as a string.
|
||||
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"operator": "id",
|
||||
"operand": 12345
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Stream and user IDs
|
||||
|
||||
There are a few additional narrow/search options (new in Zulip 2.1)
|
||||
that use either stream IDs or user IDs that are not documented in the
|
||||
help center because they are primarily useful to API clients:
|
||||
@@ -105,16 +84,10 @@ help center because they are primarily useful to API clients:
|
||||
* `dm-including:1234`: Search all direct messages (1-on-1 and group)
|
||||
that include you and user ID `1234`.
|
||||
|
||||
!!! tip ""
|
||||
|
||||
A user ID can be found by [viewing a user's profile][view-profile]
|
||||
in the web or desktop apps. A stream ID can be found when [browsing
|
||||
streams][browse-streams] in the web app via the URL.
|
||||
|
||||
The operands for these search options must be encoded either as an
|
||||
integer ID or a JSON list of integer IDs. For example, to query
|
||||
messages sent by a user 1234 to a direct message thread with yourself,
|
||||
user 1234, and user 5678, the correct JSON-encoded query is:
|
||||
messages sent by a user 1234 to a PM thread with yourself, user 1234,
|
||||
and user 5678, the correct JSON-encoded query is:
|
||||
|
||||
```json
|
||||
[
|
||||
@@ -128,6 +101,3 @@ user 1234, and user 5678, the correct JSON-encoded query is:
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
[view-profile]: /help/view-someones-profile
|
||||
[browse-streams]: /help/browse-and-subscribe-to-streams
|
||||
|
||||
@@ -36,7 +36,7 @@ Zulip Botserver starts a web server that listens to incoming messages
|
||||
from your main Zulip server. The sequence of events in a successful
|
||||
Botserver interaction are:
|
||||
|
||||
1. Your bot user is mentioned or receives a direct message:
|
||||
1. Your bot user is mentioned or receives a private message:
|
||||
|
||||
```
|
||||
@**My Bot User** hello world
|
||||
|
||||
@@ -14,9 +14,7 @@
|
||||
* [Get a message's edit history](/api/get-message-history)
|
||||
* [Update personal message flags](/api/update-message-flags)
|
||||
* [Update personal message flags for narrow](/api/update-message-flags-for-narrow)
|
||||
* [Mark all messages as read](/api/mark-all-as-read)
|
||||
* [Mark messages in a stream as read](/api/mark-stream-as-read)
|
||||
* [Mark messages in a topic as read](/api/mark-topic-as-read)
|
||||
* [Mark messages as read in bulk](/api/mark-all-as-read)
|
||||
* [Get a message's read receipts](/api/get-read-receipts)
|
||||
|
||||
#### Scheduled messages
|
||||
@@ -95,12 +93,10 @@
|
||||
* [Add a linkifier](/api/add-linkifier)
|
||||
* [Update a linkifier](/api/update-linkifier)
|
||||
* [Remove a linkifier](/api/remove-linkifier)
|
||||
* [Reorder linkifiers](/api/reorder-linkifiers)
|
||||
* [Add a code playground](/api/add-code-playground)
|
||||
* [Remove a code playground](/api/remove-code-playground)
|
||||
* [Get all custom emoji](/api/get-custom-emoji)
|
||||
* [Upload custom emoji](/api/upload-custom-emoji)
|
||||
* [Deactivate custom emoji](/api/deactivate-custom-emoji)
|
||||
* [Get all custom profile fields](/api/get-custom-profile-fields)
|
||||
* [Reorder custom profile fields](/api/reorder-custom-profile-fields)
|
||||
* [Create a custom profile field](/api/create-custom-profile-field)
|
||||
|
||||
@@ -155,45 +155,3 @@ below are for a webhook named `MyWebHook`.
|
||||
testing with live data from the service you're integrating and can help you
|
||||
spot why something isn't working or if the service is using custom HTTP
|
||||
headers.
|
||||
|
||||
## URLs
|
||||
|
||||
The base URL for an incoming webhook integration bot is
|
||||
`{{ api_url }}/v1/external/INTEGRATION_NAME?api_key=API_KEY` where
|
||||
`INTEGRATION_NAME` is the name of the specific webhook integration and
|
||||
`API_KEY` is the API key of the bot created by the user for the
|
||||
integration.
|
||||
|
||||
The list of existing webhook integrations can be found in
|
||||
`zerver/lib/integrations.py` (at `WEBHOOK_INTEGRATIONS`) or by browsing
|
||||
the [Integrations documentation](/integrations).
|
||||
|
||||
Parameters accepted in the URL include:
|
||||
|
||||
* `api_key`: **Required**. The API key of the bot created by the user
|
||||
for the integration. To get a bot's API key, see the [API
|
||||
keys](/api/api-keys) documentation.
|
||||
|
||||
* `stream`: The stream for the integration to send notifications to.
|
||||
Can be either the stream ID or the [URL-encoded][url-encoder] stream
|
||||
name. By default the integration will send direct messages to the
|
||||
bot's owner.
|
||||
|
||||
!!! tip ""
|
||||
|
||||
A stream ID can be found when [browsing streams][browse-streams]
|
||||
in the web app via the URL.
|
||||
|
||||
* `topic`: The topic in the specified stream for the integration to
|
||||
send notifications to. The topic should also be
|
||||
[URL-encoded][url-encoder]. By default the integration will have a
|
||||
topic configured for stream messages.
|
||||
|
||||
* `only_events`, `exclude_events`: Some incoming webhook integrations
|
||||
support these parameters to filter which events will trigger a
|
||||
notification. For details, see the integration's [integration
|
||||
documentation](/integrations) page.
|
||||
|
||||
[browse-streams]: /help/browse-and-subscribe-to-streams
|
||||
[add-bot]: /help/add-a-bot-or-integration
|
||||
[url-encoder]: https://www.urlencoder.org/
|
||||
|
||||
@@ -171,7 +171,7 @@ validate the message and do the following:
|
||||
|
||||
* Send a public (stream) message if the `stream` query parameter is
|
||||
specified in the webhook URL.
|
||||
* If the `stream` query parameter isn't specified, it will send a direct
|
||||
* If the `stream` query parameter isn't specified, it will send a private
|
||||
message to the owner of the webhook bot.
|
||||
|
||||
Finally, we return a 200 http status with a JSON format success message via
|
||||
@@ -429,9 +429,8 @@ Learn how Zulip integrations work with this simple Hello World example!
|
||||
by default in the Zulip development environment. If you are running
|
||||
Zulip in production, you should make sure that this stream exists.
|
||||
|
||||
1. {!create-an-incoming-webhook.md!}
|
||||
1. {!create-bot-construct-url.md!}
|
||||
|
||||
1. {!generate-integration-url.md!}
|
||||
|
||||
1. To trigger a notification using this example webhook, you can use
|
||||
`send_webhook_fixture_message` from a [Zulip development
|
||||
@@ -456,7 +455,7 @@ Learn how Zulip integrations work with this simple Hello World example!
|
||||
|
||||
```
|
||||
|
||||
`{!create-an-incoming-webhook.md!}` and `{!congrats.md!}` are examples of
|
||||
`{!create-bot-construct-url.md!}` and `{!congrats.md!}` are examples of
|
||||
a Markdown macro. Zulip has a macro-based Markdown/Jinja2 framework that
|
||||
includes macros for common instructions in Zulip's webhooks/integrations
|
||||
documentation.
|
||||
@@ -628,8 +627,8 @@ payloads, the absence of such a header usually indicates a configuration
|
||||
issue, where one either entered the URL for a different integration, or happens to
|
||||
be running an older version of the integration that doesn't set that header.
|
||||
|
||||
If the requisite header is missing, this function sends a direct message to the
|
||||
owner of the webhook bot, notifying them of the missing header.
|
||||
If the requisite header is missing, this function sends a PM to the owner of the
|
||||
webhook bot, notifying them of the missing header.
|
||||
|
||||
### Handling unexpected webhook event types
|
||||
|
||||
|
||||
89
api_docs/mark-all-as-read.md
Normal file
89
api_docs/mark-all-as-read.md
Normal file
@@ -0,0 +1,89 @@
|
||||
{generate_api_header(/mark_all_as_read:post)}
|
||||
|
||||
## Usage examples
|
||||
|
||||
{start_tabs}
|
||||
|
||||
{generate_code_example(python)|/mark_all_as_read:post|example}
|
||||
|
||||
{generate_code_example(javascript)|/mark_all_as_read:post|example}
|
||||
|
||||
{tab|curl}
|
||||
|
||||
{generate_code_example(curl)|/mark_all_as_read:post|example}
|
||||
|
||||
{end_tabs}
|
||||
|
||||
## Parameters
|
||||
|
||||
{generate_api_arguments_table|zulip.yaml|/mark_all_as_read:post}
|
||||
|
||||
{generate_parameter_description(/mark_all_as_read:post)}
|
||||
|
||||
## Response
|
||||
|
||||
{generate_response_description(/mark_all_as_read:post)}
|
||||
|
||||
#### Example response(s)
|
||||
|
||||
{generate_code_example|/mark_all_as_read:post|fixture}
|
||||
|
||||
{generate_api_header(/mark_stream_as_read:post)}
|
||||
|
||||
## Usage examples
|
||||
|
||||
{start_tabs}
|
||||
|
||||
{generate_code_example(python)|/mark_stream_as_read:post|example}
|
||||
|
||||
{generate_code_example(javascript)|/mark_all_as_read:post|example}
|
||||
|
||||
{tab|curl}
|
||||
|
||||
{generate_code_example(curl)|/mark_stream_as_read:post|example}
|
||||
|
||||
{end_tabs}
|
||||
|
||||
## Parameters
|
||||
|
||||
{generate_api_arguments_table|zulip.yaml|/mark_stream_as_read:post}
|
||||
|
||||
{generate_parameter_description(/mark_all_as_read:post)}
|
||||
|
||||
## Response
|
||||
|
||||
{generate_response_description(/mark_all_as_read:post)}
|
||||
|
||||
#### Example response(s)
|
||||
|
||||
{generate_code_example|/mark_stream_as_read:post|fixture}
|
||||
|
||||
{generate_api_header(/mark_topic_as_read:post)}
|
||||
|
||||
## Usage examples
|
||||
|
||||
{start_tabs}
|
||||
|
||||
{generate_code_example(python)|/mark_topic_as_read:post|example}
|
||||
|
||||
{generate_code_example(javascript)|/mark_all_as_read:post|example}
|
||||
|
||||
{tab|curl}
|
||||
|
||||
{generate_code_example(curl)|/mark_topic_as_read:post|example}
|
||||
|
||||
{end_tabs}
|
||||
|
||||
## Parameters
|
||||
|
||||
{generate_api_arguments_table|zulip.yaml|/mark_topic_as_read:post}
|
||||
|
||||
{generate_parameter_description(/mark_all_as_read:post)}
|
||||
|
||||
## Response
|
||||
|
||||
{generate_response_description(/mark_all_as_read:post)}
|
||||
|
||||
#### Example response(s)
|
||||
|
||||
{generate_code_example|/mark_topic_as_read:post|fixture}
|
||||
@@ -31,7 +31,7 @@ There are currently two ways to trigger an outgoing webhook:
|
||||
|
||||
1. **@-mention** the bot user in a stream. If the bot replies, its
|
||||
reply will be sent to that stream and topic.
|
||||
2. **Send a direct message** with the bot as one of the recipients.
|
||||
2. **Send a private message** with the bot as one of the recipients.
|
||||
If the bot replies, its reply will be sent to that thread.
|
||||
|
||||
## Timeouts
|
||||
|
||||
@@ -148,7 +148,7 @@ With this API, you *cannot*
|
||||
|
||||
* modify an intercepted message (you have to send a new message).
|
||||
* send messages on behalf of or impersonate other users.
|
||||
* intercept direct messages (except for direct messages with the bot as an
|
||||
* intercept private messages (except for PMs with the bot as an
|
||||
explicit recipient).
|
||||
|
||||
### usage
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Copyright: (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
|
||||
|
||||
__revision__ = "$Id: models.py 28 2009-10-22 15:03:02Z jarek.zgoda $"
|
||||
import datetime
|
||||
import secrets
|
||||
from base64 import b32encode
|
||||
from datetime import timedelta
|
||||
from typing import List, Mapping, Optional, Union, cast
|
||||
from typing import List, Mapping, Optional, Union
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
@@ -16,7 +16,6 @@ from django.http import HttpRequest, HttpResponse
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import TypeAlias, override
|
||||
|
||||
from confirmation import settings as confirmation_settings
|
||||
from zerver.lib.types import UnspecifiedValue
|
||||
@@ -30,12 +29,6 @@ from zerver.models import (
|
||||
UserProfile,
|
||||
)
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
from zilencer.models import (
|
||||
PreregistrationRemoteRealmBillingUser,
|
||||
PreregistrationRemoteServerBillingUser,
|
||||
)
|
||||
|
||||
|
||||
class ConfirmationKeyError(Exception):
|
||||
WRONG_LENGTH = 1
|
||||
@@ -62,7 +55,7 @@ def generate_key() -> str:
|
||||
return b32encode(secrets.token_bytes(15)).decode().lower()
|
||||
|
||||
|
||||
NoZilencerConfirmationObjT: TypeAlias = Union[
|
||||
ConfirmationObjT = Union[
|
||||
MultiuseInvite,
|
||||
PreregistrationRealm,
|
||||
PreregistrationUser,
|
||||
@@ -70,13 +63,6 @@ NoZilencerConfirmationObjT: TypeAlias = Union[
|
||||
UserProfile,
|
||||
RealmReactivationStatus,
|
||||
]
|
||||
ZilencerConfirmationObjT: TypeAlias = Union[
|
||||
NoZilencerConfirmationObjT,
|
||||
"PreregistrationRemoteServerBillingUser",
|
||||
"PreregistrationRemoteRealmBillingUser",
|
||||
]
|
||||
|
||||
ConfirmationObjT = Union[NoZilencerConfirmationObjT, ZilencerConfirmationObjT]
|
||||
|
||||
|
||||
def get_object_from_key(
|
||||
@@ -130,20 +116,15 @@ def create_confirmation_link(
|
||||
*,
|
||||
validity_in_minutes: Union[Optional[int], UnspecifiedValue] = UnspecifiedValue(),
|
||||
url_args: Mapping[str, str] = {},
|
||||
no_associated_realm_object: bool = False,
|
||||
realm_creation: bool = False,
|
||||
) -> str:
|
||||
# validity_in_minutes is an override for the default values which are
|
||||
# determined by the confirmation_type - its main purpose is for use
|
||||
# in tests which may want to have control over the exact expiration time.
|
||||
key = generate_key()
|
||||
|
||||
# Some confirmation objects, like those for realm creation or those used
|
||||
# for the self-hosted management flows, are not associated with a realm
|
||||
# hosted by this Zulip server.
|
||||
if no_associated_realm_object:
|
||||
if realm_creation:
|
||||
realm = None
|
||||
else:
|
||||
obj = cast(NoZilencerConfirmationObjT, obj)
|
||||
assert not isinstance(obj, PreregistrationRealm)
|
||||
realm = obj.realm
|
||||
|
||||
@@ -154,9 +135,11 @@ def create_confirmation_link(
|
||||
expiry_date = None
|
||||
else:
|
||||
assert validity_in_minutes is not None
|
||||
expiry_date = current_time + timedelta(minutes=validity_in_minutes)
|
||||
expiry_date = current_time + datetime.timedelta(minutes=validity_in_minutes)
|
||||
else:
|
||||
expiry_date = current_time + timedelta(days=_properties[confirmation_type].validity_in_days)
|
||||
expiry_date = current_time + datetime.timedelta(
|
||||
days=_properties[confirmation_type].validity_in_days
|
||||
)
|
||||
|
||||
Confirmation.objects.create(
|
||||
content_object=obj,
|
||||
@@ -201,14 +184,11 @@ class Confirmation(models.Model):
|
||||
MULTIUSE_INVITE = 6
|
||||
REALM_CREATION = 7
|
||||
REALM_REACTIVATION = 8
|
||||
REMOTE_SERVER_BILLING_LEGACY_LOGIN = 9
|
||||
REMOTE_REALM_BILLING_LEGACY_LOGIN = 10
|
||||
type = models.PositiveSmallIntegerField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ("type", "confirmation_key")
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.content_object!r}"
|
||||
|
||||
@@ -239,13 +219,6 @@ _properties = {
|
||||
Confirmation.REALM_CREATION: ConfirmationType("get_prereg_key_and_redirect"),
|
||||
Confirmation.REALM_REACTIVATION: ConfirmationType("realm_reactivation"),
|
||||
}
|
||||
if settings.ZILENCER_ENABLED:
|
||||
_properties[Confirmation.REMOTE_SERVER_BILLING_LEGACY_LOGIN] = ConfirmationType(
|
||||
"remote_billing_legacy_server_from_login_confirmation_link"
|
||||
)
|
||||
_properties[Confirmation.REMOTE_REALM_BILLING_LEGACY_LOGIN] = ConfirmationType(
|
||||
"remote_realm_billing_from_login_confirmation_link"
|
||||
)
|
||||
|
||||
|
||||
def one_click_unsubscribe_link(user_profile: UserProfile, email_type: str) -> str:
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.utils.timezone import now as timezone_now
|
||||
|
||||
from corporate.lib.stripe import (
|
||||
RealmBillingSession,
|
||||
RemoteRealmBillingSession,
|
||||
RemoteServerBillingSession,
|
||||
)
|
||||
from corporate.models import Customer, CustomerPlan
|
||||
from zerver.lib.utils import assert_is_not_none
|
||||
|
||||
|
||||
@dataclass
|
||||
class RemoteActivityPlanData:
|
||||
current_status: str
|
||||
current_plan_name: str
|
||||
annual_revenue: int
|
||||
|
||||
|
||||
def get_realms_with_default_discount_dict() -> Dict[str, Decimal]:
|
||||
realms_with_default_discount: Dict[str, Any] = {}
|
||||
customers = (
|
||||
Customer.objects.exclude(default_discount=None)
|
||||
.exclude(default_discount=0)
|
||||
.exclude(realm=None)
|
||||
)
|
||||
for customer in customers:
|
||||
assert customer.realm is not None
|
||||
realms_with_default_discount[customer.realm.string_id] = assert_is_not_none(
|
||||
customer.default_discount
|
||||
)
|
||||
return realms_with_default_discount
|
||||
|
||||
|
||||
def estimate_annual_recurring_revenue_by_realm() -> Dict[str, int]: # nocoverage
|
||||
annual_revenue = {}
|
||||
for plan in CustomerPlan.objects.filter(status=CustomerPlan.ACTIVE).select_related(
|
||||
"customer__realm"
|
||||
):
|
||||
if plan.customer.realm is not None:
|
||||
# TODO: figure out what to do for plans that don't automatically
|
||||
# renew, but which probably will renew
|
||||
renewal_cents = RealmBillingSession(
|
||||
realm=plan.customer.realm
|
||||
).get_customer_plan_renewal_amount(plan, timezone_now())
|
||||
if plan.billing_schedule == CustomerPlan.BILLING_SCHEDULE_MONTHLY:
|
||||
renewal_cents *= 12
|
||||
# TODO: Decimal stuff
|
||||
annual_revenue[plan.customer.realm.string_id] = int(renewal_cents / 100)
|
||||
return annual_revenue
|
||||
|
||||
|
||||
def get_plan_data_by_remote_server() -> Dict[int, RemoteActivityPlanData]: # nocoverage
|
||||
remote_server_plan_data: Dict[int, RemoteActivityPlanData] = {}
|
||||
for plan in CustomerPlan.objects.filter(
|
||||
status__lt=CustomerPlan.LIVE_STATUS_THRESHOLD, customer__realm__isnull=True
|
||||
).select_related("customer__remote_server", "customer__remote_realm"):
|
||||
renewal_cents = 0
|
||||
server_id = None
|
||||
|
||||
if plan.customer.remote_server is not None:
|
||||
server_id = plan.customer.remote_server.id
|
||||
renewal_cents = RemoteServerBillingSession(
|
||||
remote_server=plan.customer.remote_server
|
||||
).get_customer_plan_renewal_amount(plan, timezone_now())
|
||||
elif plan.customer.remote_realm is not None:
|
||||
server_id = plan.customer.remote_realm.server.id
|
||||
renewal_cents = RemoteRealmBillingSession(
|
||||
remote_realm=plan.customer.remote_realm
|
||||
).get_customer_plan_renewal_amount(plan, timezone_now())
|
||||
|
||||
assert server_id is not None
|
||||
|
||||
if plan.billing_schedule == CustomerPlan.BILLING_SCHEDULE_MONTHLY:
|
||||
renewal_cents *= 12
|
||||
|
||||
current_data = remote_server_plan_data.get(server_id)
|
||||
if current_data is not None:
|
||||
current_revenue = remote_server_plan_data[server_id].annual_revenue
|
||||
remote_server_plan_data[server_id] = RemoteActivityPlanData(
|
||||
current_status="Multiple plans",
|
||||
current_plan_name="See support view",
|
||||
annual_revenue=current_revenue + int(renewal_cents / 100),
|
||||
)
|
||||
else:
|
||||
remote_server_plan_data[server_id] = RemoteActivityPlanData(
|
||||
current_status=plan.get_plan_status_as_text(),
|
||||
current_plan_name=plan.name,
|
||||
annual_revenue=int(renewal_cents / 100),
|
||||
)
|
||||
return remote_server_plan_data
|
||||
@@ -1,187 +0,0 @@
|
||||
from functools import wraps
|
||||
from typing import Callable, Optional
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from typing_extensions import Concatenate, ParamSpec
|
||||
|
||||
from corporate.lib.remote_billing_util import (
|
||||
RemoteBillingIdentityExpiredError,
|
||||
get_remote_realm_and_user_from_session,
|
||||
get_remote_server_and_user_from_session,
|
||||
)
|
||||
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
|
||||
from zerver.lib.exceptions import RemoteBillingAuthenticationError
|
||||
from zerver.lib.subdomains import get_subdomain
|
||||
from zerver.lib.url_encoding import append_url_query_string
|
||||
from zilencer.models import RemoteRealm
|
||||
|
||||
ParamT = ParamSpec("ParamT")
|
||||
|
||||
|
||||
def is_self_hosting_management_subdomain(request: HttpRequest) -> bool:
|
||||
subdomain = get_subdomain(request)
|
||||
return subdomain == settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN
|
||||
|
||||
|
||||
def self_hosting_management_endpoint(
|
||||
view_func: Callable[Concatenate[HttpRequest, ParamT], HttpResponse]
|
||||
) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]:
|
||||
@wraps(view_func)
|
||||
def _wrapped_view_func(
|
||||
request: HttpRequest, /, *args: ParamT.args, **kwargs: ParamT.kwargs
|
||||
) -> HttpResponse:
|
||||
if not is_self_hosting_management_subdomain(request): # nocoverage
|
||||
return render(request, "404.html", status=404)
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return _wrapped_view_func
|
||||
|
||||
|
||||
def authenticated_remote_realm_management_endpoint(
|
||||
view_func: Callable[Concatenate[HttpRequest, RemoteRealmBillingSession, ParamT], HttpResponse]
|
||||
) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]:
|
||||
@wraps(view_func)
|
||||
def _wrapped_view_func(
|
||||
request: HttpRequest,
|
||||
/,
|
||||
*args: ParamT.args,
|
||||
**kwargs: ParamT.kwargs,
|
||||
) -> HttpResponse:
|
||||
if not is_self_hosting_management_subdomain(request): # nocoverage
|
||||
return render(request, "404.html", status=404)
|
||||
|
||||
realm_uuid = kwargs.get("realm_uuid")
|
||||
if realm_uuid is not None and not isinstance(realm_uuid, str): # nocoverage
|
||||
raise TypeError("realm_uuid must be a string or None")
|
||||
|
||||
try:
|
||||
remote_realm, remote_billing_user = get_remote_realm_and_user_from_session(
|
||||
request, realm_uuid
|
||||
)
|
||||
except RemoteBillingIdentityExpiredError as e:
|
||||
# The user had an authenticated session with an identity_dict,
|
||||
# but it expired.
|
||||
# We want to redirect back to the start of their login flow
|
||||
# at their {realm.host}/self-hosted-billing/ with a proper
|
||||
# next parameter to take them back to what they're trying
|
||||
# to access after re-authing.
|
||||
# Note: Theoretically we could take the realm_uuid from the request
|
||||
# path or params to figure out the remote_realm.host for the redirect,
|
||||
# but that would mean leaking that .host value to anyone who knows
|
||||
# the uuid. Therefore we limit ourselves to taking the realm_uuid
|
||||
# from the identity_dict - since that proves that the user at least
|
||||
# previously was successfully authenticated as a billing admin of that
|
||||
# realm.
|
||||
realm_uuid = e.realm_uuid
|
||||
server_uuid = e.server_uuid
|
||||
uri_scheme = e.uri_scheme
|
||||
if realm_uuid is None:
|
||||
# This doesn't make sense - if get_remote_realm_and_user_from_session
|
||||
# found an expired identity dict, it should have had a realm_uuid.
|
||||
raise AssertionError
|
||||
|
||||
assert server_uuid is not None, "identity_dict with realm_uuid must have server_uuid"
|
||||
assert uri_scheme is not None, "identity_dict with realm_uuid must have uri_scheme"
|
||||
|
||||
try:
|
||||
remote_realm = RemoteRealm.objects.get(uuid=realm_uuid, server__uuid=server_uuid)
|
||||
except RemoteRealm.DoesNotExist:
|
||||
# This should be impossible - unless the RemoteRealm existed and somehow the row
|
||||
# was deleted.
|
||||
raise AssertionError
|
||||
|
||||
# Using EXTERNAL_URI_SCHEME means we'll do https:// in production, which is
|
||||
# the sane default - while having http:// in development, which will allow
|
||||
# these redirects to work there for testing.
|
||||
url = urljoin(uri_scheme + remote_realm.host, "/self-hosted-billing/")
|
||||
|
||||
page_type = get_next_page_param_from_request_path(request)
|
||||
if page_type is not None:
|
||||
query = urlencode({"next_page": page_type})
|
||||
url = append_url_query_string(url, query)
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
billing_session = RemoteRealmBillingSession(
|
||||
remote_realm, remote_billing_user=remote_billing_user
|
||||
)
|
||||
return view_func(request, billing_session)
|
||||
|
||||
return _wrapped_view_func
|
||||
|
||||
|
||||
def get_next_page_param_from_request_path(request: HttpRequest) -> Optional[str]:
|
||||
# Our endpoint URLs in this subsystem end with something like
|
||||
# /sponsorship or /plans etc.
|
||||
# Therefore we can use this nice property to figure out easily what
|
||||
# kind of page the user is trying to access and find the right value
|
||||
# for the `next` query parameter.
|
||||
path = request.path
|
||||
if path.endswith("/"):
|
||||
path = path[:-1]
|
||||
|
||||
page_type = path.split("/")[-1]
|
||||
|
||||
from corporate.views.remote_billing_page import (
|
||||
VALID_NEXT_PAGES as REMOTE_BILLING_VALID_NEXT_PAGES,
|
||||
)
|
||||
|
||||
if page_type in REMOTE_BILLING_VALID_NEXT_PAGES:
|
||||
return page_type
|
||||
|
||||
# Should be impossible to reach here. If this is reached, it must mean
|
||||
# we have a registered endpoint that doesn't have a VALID_NEXT_PAGES entry
|
||||
# or the parsing logic above is failing.
|
||||
raise AssertionError(f"Unknown page type: {page_type}")
|
||||
|
||||
|
||||
def authenticated_remote_server_management_endpoint(
|
||||
view_func: Callable[Concatenate[HttpRequest, RemoteServerBillingSession, ParamT], HttpResponse]
|
||||
) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]:
|
||||
@wraps(view_func)
|
||||
def _wrapped_view_func(
|
||||
request: HttpRequest,
|
||||
/,
|
||||
*args: ParamT.args,
|
||||
**kwargs: ParamT.kwargs,
|
||||
) -> HttpResponse:
|
||||
if not is_self_hosting_management_subdomain(request): # nocoverage
|
||||
return render(request, "404.html", status=404)
|
||||
|
||||
server_uuid = kwargs.get("server_uuid")
|
||||
if not isinstance(server_uuid, str):
|
||||
raise TypeError("server_uuid must be a string") # nocoverage
|
||||
|
||||
try:
|
||||
remote_server, remote_billing_user = get_remote_server_and_user_from_session(
|
||||
request, server_uuid=server_uuid
|
||||
)
|
||||
if remote_billing_user is None:
|
||||
# This should only be possible if the user hasn't finished the confirmation flow
|
||||
# and doesn't have a fully authenticated session yet. They should not be attempting
|
||||
# to access this endpoint yet.
|
||||
raise RemoteBillingAuthenticationError
|
||||
except (RemoteBillingIdentityExpiredError, RemoteBillingAuthenticationError):
|
||||
# In this flow, we can only redirect to our local "legacy server flow login" page.
|
||||
# That means that we can do it universally whether the user has an expired
|
||||
# identity_dict, or just lacks any form of authentication info at all - there
|
||||
# are no security concerns since this is just a local redirect.
|
||||
url = reverse("remote_billing_legacy_server_login")
|
||||
page_type = get_next_page_param_from_request_path(request)
|
||||
if page_type is not None:
|
||||
query = urlencode({"next_page": page_type})
|
||||
url = append_url_query_string(url, query)
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
assert remote_billing_user is not None
|
||||
billing_session = RemoteServerBillingSession(
|
||||
remote_server, remote_billing_user=remote_billing_user
|
||||
)
|
||||
return view_func(request, billing_session)
|
||||
|
||||
return _wrapped_view_func
|
||||
@@ -7,8 +7,7 @@ from corporate.lib.stripe import LicenseLimitError, get_latest_seat_count, get_s
|
||||
from corporate.models import get_current_plan_by_realm
|
||||
from zerver.actions.create_user import send_message_to_signup_notification_stream
|
||||
from zerver.lib.exceptions import InvitationError
|
||||
from zerver.models import Realm, UserProfile
|
||||
from zerver.models.users import get_system_bot
|
||||
from zerver.models import Realm, UserProfile, get_system_bot
|
||||
|
||||
|
||||
def generate_licenses_low_warning_message_if_required(realm: Realm) -> Optional[str]:
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import logging
|
||||
from typing import Literal, Optional, Tuple, TypedDict, Union, cast
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from zerver.lib.exceptions import JsonableError, RemoteBillingAuthenticationError
|
||||
from zerver.lib.timestamp import datetime_to_timestamp
|
||||
from zilencer.models import (
|
||||
RemoteRealm,
|
||||
RemoteRealmBillingUser,
|
||||
RemoteServerBillingUser,
|
||||
RemoteZulipServer,
|
||||
)
|
||||
|
||||
billing_logger = logging.getLogger("corporate.stripe")
|
||||
|
||||
# The sessions are relatively short-lived, so that we can avoid issues
|
||||
# with users who have their privileges revoked on the remote server
|
||||
# maintaining access to the billing page for too long.
|
||||
REMOTE_BILLING_SESSION_VALIDITY_SECONDS = 2 * 60 * 60
|
||||
|
||||
|
||||
class RemoteBillingUserDict(TypedDict):
|
||||
user_uuid: str
|
||||
user_email: str
|
||||
user_full_name: str
|
||||
|
||||
|
||||
class RemoteBillingIdentityDict(TypedDict):
|
||||
user: RemoteBillingUserDict
|
||||
remote_server_uuid: str
|
||||
remote_realm_uuid: str
|
||||
|
||||
remote_billing_user_id: Optional[int]
|
||||
authenticated_at: int
|
||||
uri_scheme: Literal["http://", "https://"]
|
||||
|
||||
next_page: Optional[str]
|
||||
|
||||
|
||||
class LegacyServerIdentityDict(TypedDict):
|
||||
# Currently this has only one field. We can extend this
|
||||
# to add more information as appropriate.
|
||||
remote_server_uuid: str
|
||||
|
||||
remote_billing_user_id: Optional[int]
|
||||
authenticated_at: int
|
||||
|
||||
|
||||
class RemoteBillingIdentityExpiredError(Exception):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
realm_uuid: Optional[str] = None,
|
||||
server_uuid: Optional[str] = None,
|
||||
uri_scheme: Optional[Literal["http://", "https://"]] = None,
|
||||
) -> None:
|
||||
self.realm_uuid = realm_uuid
|
||||
self.server_uuid = server_uuid
|
||||
self.uri_scheme = uri_scheme
|
||||
|
||||
|
||||
def get_identity_dict_from_session(
|
||||
request: HttpRequest,
|
||||
*,
|
||||
realm_uuid: Optional[str],
|
||||
server_uuid: Optional[str],
|
||||
) -> Optional[Union[RemoteBillingIdentityDict, LegacyServerIdentityDict]]:
|
||||
if not (realm_uuid or server_uuid):
|
||||
return None
|
||||
|
||||
identity_dicts = request.session.get("remote_billing_identities")
|
||||
if identity_dicts is None:
|
||||
return None
|
||||
|
||||
if realm_uuid is not None:
|
||||
result = identity_dicts.get(f"remote_realm:{realm_uuid}")
|
||||
else:
|
||||
assert server_uuid is not None
|
||||
result = identity_dicts.get(f"remote_server:{server_uuid}")
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
if (
|
||||
datetime_to_timestamp(timezone_now()) - result["authenticated_at"]
|
||||
> REMOTE_BILLING_SESSION_VALIDITY_SECONDS
|
||||
):
|
||||
# In this case we raise, because callers want to catch this as an explicitly
|
||||
# different scenario from the user not being authenticated, to handle it nicely
|
||||
# by redirecting them to their login page.
|
||||
raise RemoteBillingIdentityExpiredError(
|
||||
realm_uuid=result.get("remote_realm_uuid"),
|
||||
server_uuid=result.get("remote_server_uuid"),
|
||||
uri_scheme=result.get("uri_scheme"),
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_remote_realm_and_user_from_session(
|
||||
request: HttpRequest,
|
||||
realm_uuid: Optional[str],
|
||||
) -> Tuple[RemoteRealm, RemoteRealmBillingUser]:
|
||||
# Cannot use isinstance with TypeDicts, to make mypy know
|
||||
# which of the TypedDicts in the Union this is - so just cast it.
|
||||
identity_dict = cast(
|
||||
Optional[RemoteBillingIdentityDict],
|
||||
get_identity_dict_from_session(request, realm_uuid=realm_uuid, server_uuid=None),
|
||||
)
|
||||
|
||||
if identity_dict is None:
|
||||
raise RemoteBillingAuthenticationError
|
||||
|
||||
remote_server_uuid = identity_dict["remote_server_uuid"]
|
||||
remote_realm_uuid = identity_dict["remote_realm_uuid"]
|
||||
|
||||
try:
|
||||
remote_realm = RemoteRealm.objects.get(
|
||||
uuid=remote_realm_uuid, server__uuid=remote_server_uuid
|
||||
)
|
||||
except RemoteRealm.DoesNotExist:
|
||||
raise AssertionError(
|
||||
"The remote realm is missing despite being in the RemoteBillingIdentityDict"
|
||||
)
|
||||
|
||||
if (
|
||||
remote_realm.registration_deactivated
|
||||
or remote_realm.realm_deactivated
|
||||
or remote_realm.server.deactivated
|
||||
):
|
||||
raise JsonableError(_("Registration is deactivated"))
|
||||
|
||||
remote_billing_user_id = identity_dict["remote_billing_user_id"]
|
||||
# We only put IdentityDicts with remote_billing_user_id in the session in this flow,
|
||||
# because the RemoteRealmBillingUser already exists when this is inserted into the session
|
||||
# at the end of authentication.
|
||||
assert remote_billing_user_id is not None
|
||||
|
||||
try:
|
||||
remote_billing_user = RemoteRealmBillingUser.objects.get(
|
||||
id=remote_billing_user_id, remote_realm=remote_realm
|
||||
)
|
||||
except RemoteRealmBillingUser.DoesNotExist:
|
||||
raise AssertionError
|
||||
|
||||
return remote_realm, remote_billing_user
|
||||
|
||||
|
||||
def get_remote_server_and_user_from_session(
|
||||
request: HttpRequest,
|
||||
server_uuid: str,
|
||||
) -> Tuple[RemoteZulipServer, Optional[RemoteServerBillingUser]]:
|
||||
identity_dict: Optional[LegacyServerIdentityDict] = get_identity_dict_from_session(
|
||||
request, realm_uuid=None, server_uuid=server_uuid
|
||||
)
|
||||
|
||||
if identity_dict is None:
|
||||
raise RemoteBillingAuthenticationError
|
||||
|
||||
remote_server_uuid = identity_dict["remote_server_uuid"]
|
||||
try:
|
||||
remote_server = RemoteZulipServer.objects.get(uuid=remote_server_uuid)
|
||||
except RemoteZulipServer.DoesNotExist:
|
||||
raise JsonableError(_("Invalid remote server."))
|
||||
|
||||
if remote_server.deactivated:
|
||||
raise JsonableError(_("Registration is deactivated"))
|
||||
|
||||
remote_billing_user_id = identity_dict.get("remote_billing_user_id")
|
||||
if remote_billing_user_id is None:
|
||||
return remote_server, None
|
||||
|
||||
try:
|
||||
remote_billing_user = RemoteServerBillingUser.objects.get(
|
||||
id=remote_billing_user_id, remote_server=remote_server
|
||||
)
|
||||
except RemoteServerBillingUser.DoesNotExist:
|
||||
remote_billing_user = None
|
||||
|
||||
return remote_server, remote_billing_user
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,19 @@
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, Optional, Union
|
||||
from contextlib import suppress
|
||||
from typing import Any, Callable, Dict, Union
|
||||
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
|
||||
from corporate.lib.stripe import (
|
||||
BillingError,
|
||||
InvalidPlanUpgradeError,
|
||||
RealmBillingSession,
|
||||
RemoteRealmBillingSession,
|
||||
RemoteServerBillingSession,
|
||||
UpgradeWithExistingPlanError,
|
||||
ensure_realm_does_not_have_active_plan,
|
||||
process_initial_upgrade,
|
||||
update_or_create_stripe_customer,
|
||||
)
|
||||
from corporate.models import Customer, CustomerPlan, Event, PaymentIntent, Session
|
||||
from zerver.models.users import get_active_user_profile_by_id_in_realm
|
||||
from corporate.models import Event, PaymentIntent, Session
|
||||
from zerver.models import get_active_user_profile_by_id_in_realm
|
||||
|
||||
billing_logger = logging.getLogger("corporate.stripe")
|
||||
|
||||
@@ -63,20 +63,6 @@ def error_handler(
|
||||
return wrapper
|
||||
|
||||
|
||||
def get_billing_session_for_stripe_webhook(
|
||||
customer: Customer, user_id: Optional[str]
|
||||
) -> Union[RealmBillingSession, RemoteRealmBillingSession, RemoteServerBillingSession]:
|
||||
if customer.remote_realm is not None: # nocoverage
|
||||
return RemoteRealmBillingSession(customer.remote_realm)
|
||||
elif customer.remote_server is not None: # nocoverage
|
||||
return RemoteServerBillingSession(customer.remote_server)
|
||||
else:
|
||||
assert user_id is not None
|
||||
assert customer.realm is not None
|
||||
user = get_active_user_profile_by_id_in_realm(int(user_id), customer.realm)
|
||||
return RealmBillingSession(user)
|
||||
|
||||
|
||||
@error_handler
|
||||
def handle_checkout_session_completed_event(
|
||||
stripe_session: stripe.checkout.Session, session: Session
|
||||
@@ -84,20 +70,45 @@ def handle_checkout_session_completed_event(
|
||||
session.status = Session.COMPLETED
|
||||
session.save()
|
||||
|
||||
assert isinstance(stripe_session.setup_intent, str)
|
||||
assert stripe_session.metadata is not None
|
||||
stripe_setup_intent = stripe.SetupIntent.retrieve(stripe_session.setup_intent)
|
||||
billing_session = get_billing_session_for_stripe_webhook(
|
||||
session.customer, stripe_session.metadata.get("user_id")
|
||||
)
|
||||
assert session.customer.realm is not None
|
||||
user_id = stripe_session.metadata.get("user_id")
|
||||
assert user_id is not None
|
||||
user = get_active_user_profile_by_id_in_realm(user_id, session.customer.realm)
|
||||
payment_method = stripe_setup_intent.payment_method
|
||||
assert isinstance(payment_method, (str, type(None)))
|
||||
|
||||
if session.type in [
|
||||
Session.CARD_UPDATE_FROM_BILLING_PAGE,
|
||||
Session.CARD_UPDATE_FROM_UPGRADE_PAGE,
|
||||
Session.UPGRADE_FROM_BILLING_PAGE,
|
||||
Session.RETRY_UPGRADE_WITH_ANOTHER_PAYMENT_METHOD,
|
||||
]:
|
||||
billing_session.update_or_create_stripe_customer(payment_method)
|
||||
ensure_realm_does_not_have_active_plan(user.realm)
|
||||
update_or_create_stripe_customer(user, payment_method)
|
||||
assert session.payment_intent is not None
|
||||
session.payment_intent.status = PaymentIntent.PROCESSING
|
||||
session.payment_intent.last_payment_error = ()
|
||||
session.payment_intent.save(update_fields=["status", "last_payment_error"])
|
||||
with suppress(stripe.error.CardError):
|
||||
stripe.PaymentIntent.confirm(
|
||||
session.payment_intent.stripe_payment_intent_id,
|
||||
payment_method=payment_method,
|
||||
off_session=True,
|
||||
)
|
||||
elif session.type in [
|
||||
Session.FREE_TRIAL_UPGRADE_FROM_BILLING_PAGE,
|
||||
Session.FREE_TRIAL_UPGRADE_FROM_ONBOARDING_PAGE,
|
||||
]:
|
||||
ensure_realm_does_not_have_active_plan(user.realm)
|
||||
update_or_create_stripe_customer(user, payment_method)
|
||||
process_initial_upgrade(
|
||||
user,
|
||||
int(stripe_setup_intent.metadata["licenses"]),
|
||||
stripe_setup_intent.metadata["license_management"] == "automatic",
|
||||
int(stripe_setup_intent.metadata["billing_schedule"]),
|
||||
charge_automatically=True,
|
||||
free_trial=True,
|
||||
)
|
||||
elif session.type in [Session.CARD_UPDATE_FROM_BILLING_PAGE]:
|
||||
update_or_create_stripe_customer(user, payment_method)
|
||||
|
||||
|
||||
@error_handler
|
||||
@@ -107,12 +118,13 @@ def handle_payment_intent_succeeded_event(
|
||||
payment_intent.status = PaymentIntent.SUCCEEDED
|
||||
payment_intent.save()
|
||||
metadata: Dict[str, Any] = stripe_payment_intent.metadata
|
||||
assert payment_intent.customer.realm is not None
|
||||
user_id = metadata.get("user_id")
|
||||
assert user_id is not None
|
||||
user = get_active_user_profile_by_id_in_realm(user_id, payment_intent.customer.realm)
|
||||
|
||||
description = ""
|
||||
charge: stripe.Charge
|
||||
for charge in stripe_payment_intent.charges: # type: ignore[attr-defined] # https://stripe.com/docs/upgrades#2022-11-15
|
||||
assert charge.payment_method_details is not None
|
||||
assert charge.payment_method_details.card is not None
|
||||
for charge in stripe_payment_intent.charges:
|
||||
description = f"Payment (Card ending in {charge.payment_method_details.card.last4})"
|
||||
break
|
||||
|
||||
@@ -123,30 +135,48 @@ def handle_payment_intent_succeeded_event(
|
||||
description=description,
|
||||
discountable=False,
|
||||
)
|
||||
billing_session = get_billing_session_for_stripe_webhook(
|
||||
payment_intent.customer, metadata.get("user_id")
|
||||
)
|
||||
plan_tier = int(metadata["plan_tier"])
|
||||
try:
|
||||
billing_session.ensure_current_plan_is_upgradable(payment_intent.customer, plan_tier)
|
||||
except (UpgradeWithExistingPlanError, InvalidPlanUpgradeError) as e:
|
||||
ensure_realm_does_not_have_active_plan(user.realm)
|
||||
except UpgradeWithExistingPlanError as e:
|
||||
stripe_invoice = stripe.Invoice.create(
|
||||
auto_advance=True,
|
||||
collection_method="charge_automatically",
|
||||
customer=stripe_payment_intent.customer,
|
||||
days_until_due=None,
|
||||
statement_descriptor=CustomerPlan.name_from_tier(plan_tier).replace("Zulip ", "")
|
||||
+ " Credit",
|
||||
statement_descriptor="Zulip Cloud Standard Credit",
|
||||
)
|
||||
stripe.Invoice.finalize_invoice(stripe_invoice)
|
||||
raise e
|
||||
|
||||
billing_session.process_initial_upgrade(
|
||||
plan_tier,
|
||||
process_initial_upgrade(
|
||||
user,
|
||||
int(metadata["licenses"]),
|
||||
metadata["license_management"] == "automatic",
|
||||
int(metadata["billing_schedule"]),
|
||||
True,
|
||||
False,
|
||||
billing_session.get_remote_server_legacy_plan(payment_intent.customer),
|
||||
)
|
||||
|
||||
|
||||
@error_handler
|
||||
def handle_payment_intent_payment_failed_event(
|
||||
stripe_payment_intent: stripe.PaymentIntent, payment_intent: PaymentIntent
|
||||
) -> None:
|
||||
payment_intent.status = PaymentIntent.get_status_integer_from_status_text(
|
||||
stripe_payment_intent.status
|
||||
)
|
||||
assert payment_intent.customer.realm is not None
|
||||
billing_logger.info(
|
||||
"Stripe payment intent failed: %s %s %s %s",
|
||||
payment_intent.customer.realm.string_id,
|
||||
stripe_payment_intent.last_payment_error.get("type"),
|
||||
stripe_payment_intent.last_payment_error.get("code"),
|
||||
stripe_payment_intent.last_payment_error.get("param"),
|
||||
)
|
||||
payment_intent.last_payment_error = {
|
||||
"description": stripe_payment_intent.last_payment_error.get("type"),
|
||||
}
|
||||
payment_intent.last_payment_error["message"] = stripe_payment_intent.last_payment_error.get(
|
||||
"message"
|
||||
)
|
||||
payment_intent.save(update_fields=["status", "last_payment_error"])
|
||||
|
||||
@@ -1,148 +1,15 @@
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import Optional, TypedDict
|
||||
from urllib.parse import urlencode, urljoin, urlunsplit
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now as timezone_now
|
||||
|
||||
from corporate.lib.stripe import BillingSession
|
||||
from corporate.models import (
|
||||
Customer,
|
||||
CustomerPlan,
|
||||
ZulipSponsorshipRequest,
|
||||
get_current_plan_by_customer,
|
||||
)
|
||||
from zerver.models import Realm
|
||||
from zerver.models.realms import get_org_type_display_name, get_realm
|
||||
from zilencer.lib.remote_counts import MissingDataError
|
||||
from zerver.models import Realm, get_realm
|
||||
|
||||
|
||||
class SponsorshipRequestDict(TypedDict):
|
||||
org_type: str
|
||||
org_website: str
|
||||
org_description: str
|
||||
total_users: str
|
||||
paid_users: str
|
||||
paid_users_description: str
|
||||
requested_plan: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SponsorshipData:
|
||||
sponsorship_pending: bool = False
|
||||
default_discount: Optional[Decimal] = None
|
||||
latest_sponsorship_request: Optional[SponsorshipRequestDict] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanData:
|
||||
customer: Optional["Customer"] = None
|
||||
current_plan: Optional["CustomerPlan"] = None
|
||||
licenses: Optional[int] = None
|
||||
licenses_used: Optional[int] = None
|
||||
is_legacy_plan: bool = False
|
||||
has_fixed_price: bool = False
|
||||
warning: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupportData:
|
||||
plan_data: PlanData
|
||||
sponsorship_data: SponsorshipData
|
||||
|
||||
|
||||
def get_realm_support_url(realm: Realm) -> str:
|
||||
def get_support_url(realm: Realm) -> str:
|
||||
support_realm_uri = get_realm(settings.STAFF_SUBDOMAIN).uri
|
||||
support_url = urljoin(
|
||||
support_realm_uri,
|
||||
urlunsplit(("", "", reverse("support"), urlencode({"q": realm.string_id}), "")),
|
||||
)
|
||||
return support_url
|
||||
|
||||
|
||||
def get_customer_discount_for_support_view(
|
||||
customer: Optional[Customer] = None,
|
||||
) -> Optional[Decimal]:
|
||||
if customer is None:
|
||||
return None
|
||||
return customer.default_discount
|
||||
|
||||
|
||||
def get_customer_sponsorship_data(customer: Customer) -> SponsorshipData:
|
||||
pending = customer.sponsorship_pending
|
||||
discount = customer.default_discount
|
||||
sponsorship_request = None
|
||||
if pending:
|
||||
last_sponsorship_request = (
|
||||
ZulipSponsorshipRequest.objects.filter(customer=customer).order_by("id").last()
|
||||
)
|
||||
if last_sponsorship_request is not None:
|
||||
org_type_name = get_org_type_display_name(last_sponsorship_request.org_type)
|
||||
if (
|
||||
last_sponsorship_request.org_website is None
|
||||
or last_sponsorship_request.org_website == ""
|
||||
):
|
||||
website = "No website submitted"
|
||||
else:
|
||||
website = last_sponsorship_request.org_website
|
||||
sponsorship_request = SponsorshipRequestDict(
|
||||
org_type=org_type_name,
|
||||
org_website=website,
|
||||
org_description=last_sponsorship_request.org_description,
|
||||
total_users=last_sponsorship_request.expected_total_users,
|
||||
paid_users=last_sponsorship_request.paid_users_count,
|
||||
paid_users_description=last_sponsorship_request.paid_users_description,
|
||||
requested_plan=last_sponsorship_request.requested_plan,
|
||||
)
|
||||
|
||||
return SponsorshipData(
|
||||
sponsorship_pending=pending,
|
||||
default_discount=discount,
|
||||
latest_sponsorship_request=sponsorship_request,
|
||||
)
|
||||
|
||||
|
||||
def get_current_plan_data_for_support_view(billing_session: BillingSession) -> PlanData:
|
||||
customer = billing_session.get_customer()
|
||||
plan = None
|
||||
if customer is not None:
|
||||
plan = get_current_plan_by_customer(customer)
|
||||
plan_data = PlanData(
|
||||
customer=customer,
|
||||
current_plan=plan,
|
||||
)
|
||||
if plan is not None:
|
||||
new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(
|
||||
plan, timezone_now()
|
||||
)
|
||||
if last_ledger_entry is not None:
|
||||
if new_plan is not None:
|
||||
plan_data.current_plan = new_plan # nocoverage
|
||||
plan_data.licenses = last_ledger_entry.licenses
|
||||
try:
|
||||
plan_data.licenses_used = billing_session.current_count_for_billed_licenses()
|
||||
except MissingDataError: # nocoverage
|
||||
plan_data.warning = "Recent data missing: No information for used licenses"
|
||||
assert plan_data.current_plan is not None # for mypy
|
||||
plan_data.is_legacy_plan = (
|
||||
plan_data.current_plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY
|
||||
)
|
||||
plan_data.has_fixed_price = plan_data.current_plan.fixed_price is not None
|
||||
|
||||
return plan_data
|
||||
|
||||
|
||||
def get_data_for_support_view(billing_session: BillingSession) -> SupportData:
|
||||
plan_data = get_current_plan_data_for_support_view(billing_session)
|
||||
customer = billing_session.get_customer()
|
||||
if customer is not None:
|
||||
sponsorship_data = get_customer_sponsorship_data(customer)
|
||||
else:
|
||||
sponsorship_data = SponsorshipData()
|
||||
|
||||
return SupportData(
|
||||
plan_data=plan_data,
|
||||
sponsorship_data=sponsorship_data,
|
||||
)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 4.2.1 on 2023-05-30 03:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"corporate",
|
||||
"0017_rename_exempt_from_from_license_number_check_customer_exempt_from_license_number_check",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name="customer",
|
||||
constraint=models.CheckConstraint(
|
||||
check=models.Q(
|
||||
("realm__isnull", False), ("remote_server__isnull", False), _connector="XOR"
|
||||
),
|
||||
name="cloud_xor_self_hosted",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-11 14:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("corporate", "0018_customer_cloud_xor_self_hosted"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="zulipsponsorshiprequest",
|
||||
name="expected_total_users",
|
||||
field=models.TextField(default=""),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="zulipsponsorshiprequest",
|
||||
name="paid_users_count",
|
||||
field=models.TextField(default=""),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="zulipsponsorshiprequest",
|
||||
name="paid_users_description",
|
||||
field=models.TextField(default=""),
|
||||
),
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-17 20:11
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zilencer", "0035_remoterealmcount_remote_realm_and_more"),
|
||||
("corporate", "0019_zulipsponsorshiprequest_expected_total_users_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="customer",
|
||||
name="cloud_xor_self_hosted",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="customer",
|
||||
name="remote_realm",
|
||||
field=models.OneToOneField(
|
||||
null=True, on_delete=django.db.models.deletion.CASCADE, to="zilencer.remoterealm"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="customer",
|
||||
constraint=models.CheckConstraint(
|
||||
check=models.Q(
|
||||
("realm__isnull", False),
|
||||
("remote_server__isnull", False),
|
||||
("remote_realm__isnull", False),
|
||||
_connector="OR",
|
||||
),
|
||||
name="has_associated_model_object",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,16 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-18 14:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("corporate", "0020_add_remote_realm_customers"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="session",
|
||||
name="payment_intent",
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-21 11:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("corporate", "0021_remove_session_payment_intent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="session",
|
||||
name="is_manual_license_management_upgrade_session",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-26 16:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("corporate", "0022_session_is_manual_license_management_upgrade_session"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="zulipsponsorshiprequest",
|
||||
name="customer",
|
||||
field=models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.CASCADE, to="corporate.customer"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("corporate", "0023_zulipsponsorshiprequest_customer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
"""
|
||||
UPDATE corporate_zulipsponsorshiprequest
|
||||
SET customer_id = (
|
||||
SELECT id FROM corporate_customer WHERE corporate_customer.realm_id = corporate_zulipsponsorshiprequest.realm_id
|
||||
)
|
||||
"""
|
||||
),
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-26 16:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("corporate", "0024_zulipsponsorshiprequest_fill_customer_data"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="zulipsponsorshiprequest",
|
||||
name="customer",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="corporate.customer"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,16 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-26 16:14
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("corporate", "0025_alter_zulipsponsorshiprequest_customer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="zulipsponsorshiprequest",
|
||||
name="realm",
|
||||
),
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-28 16:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("corporate", "0026_remove_zulipsponsorshiprequest_realm"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="zulipsponsorshiprequest",
|
||||
name="requested_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# Generated by Django 4.2.8 on 2023-12-08 18:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("corporate", "0027_alter_zulipsponsorshiprequest_requested_by"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="zulipsponsorshiprequest",
|
||||
name="requested_plan",
|
||||
field=models.CharField(
|
||||
choices=[("", "UNSPECIFIED"), ("Community", "COMMUNITY"), ("Business", "BUSINESS")],
|
||||
default="",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,14 +1,12 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE, Q
|
||||
from typing_extensions import override
|
||||
from django.db.models import CASCADE
|
||||
|
||||
from zerver.models import Realm, UserProfile
|
||||
from zilencer.models import RemoteRealm, RemoteZulipServer
|
||||
from zilencer.models import RemoteZulipServer
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
@@ -18,12 +16,8 @@ class Customer(models.Model):
|
||||
and the active plan, if any.
|
||||
"""
|
||||
|
||||
# The actual model object that this customer is associated
|
||||
# with. Exactly one of the following will be non-null.
|
||||
realm = models.OneToOneField(Realm, on_delete=CASCADE, null=True)
|
||||
remote_realm = models.OneToOneField(RemoteRealm, on_delete=CASCADE, null=True)
|
||||
remote_server = models.OneToOneField(RemoteZulipServer, on_delete=CASCADE, null=True)
|
||||
|
||||
stripe_customer_id = models.CharField(max_length=255, null=True, unique=True)
|
||||
sponsorship_pending = models.BooleanField(default=False)
|
||||
# A percentage, like 85.
|
||||
@@ -34,37 +28,28 @@ class Customer(models.Model):
|
||||
# they purchased.
|
||||
exempt_from_license_number_check = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
# Enforce that at least one of these is set.
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
check=Q(realm__isnull=False)
|
||||
| Q(remote_server__isnull=False)
|
||||
| Q(remote_realm__isnull=False),
|
||||
name="has_associated_model_object",
|
||||
)
|
||||
]
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
if self.realm is not None:
|
||||
return f"{self.realm!r} (with stripe_customer_id: {self.stripe_customer_id})"
|
||||
else:
|
||||
return f"{self.remote_server!r} (with stripe_customer_id: {self.stripe_customer_id})"
|
||||
return f"{self.realm!r} {self.stripe_customer_id}"
|
||||
|
||||
@property
|
||||
def is_self_hosted(self) -> bool:
|
||||
is_self_hosted = self.remote_server is not None
|
||||
if is_self_hosted:
|
||||
assert self.realm is None
|
||||
return is_self_hosted
|
||||
|
||||
@property
|
||||
def is_cloud(self) -> bool:
|
||||
is_cloud = self.realm is not None
|
||||
if is_cloud:
|
||||
assert self.remote_server is None
|
||||
return is_cloud
|
||||
|
||||
|
||||
def get_customer_by_realm(realm: Realm) -> Optional[Customer]:
|
||||
return Customer.objects.filter(realm=realm).first()
|
||||
|
||||
|
||||
def get_customer_by_remote_server(remote_server: RemoteZulipServer) -> Optional[Customer]:
|
||||
return Customer.objects.filter(remote_server=remote_server).first()
|
||||
|
||||
|
||||
def get_customer_by_remote_realm(remote_realm: RemoteRealm) -> Optional[Customer]: # nocoverage
|
||||
return Customer.objects.filter(remote_realm=remote_realm).first()
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
stripe_event_id = models.CharField(max_length=255)
|
||||
|
||||
@@ -107,25 +92,29 @@ def get_last_associated_event_by_type(
|
||||
class Session(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
||||
stripe_session_id = models.CharField(max_length=255, unique=True)
|
||||
payment_intent = models.ForeignKey("PaymentIntent", null=True, on_delete=CASCADE)
|
||||
|
||||
UPGRADE_FROM_BILLING_PAGE = 1
|
||||
RETRY_UPGRADE_WITH_ANOTHER_PAYMENT_METHOD = 10
|
||||
FREE_TRIAL_UPGRADE_FROM_BILLING_PAGE = 20
|
||||
FREE_TRIAL_UPGRADE_FROM_ONBOARDING_PAGE = 30
|
||||
CARD_UPDATE_FROM_BILLING_PAGE = 40
|
||||
CARD_UPDATE_FROM_UPGRADE_PAGE = 50
|
||||
type = models.SmallIntegerField()
|
||||
|
||||
CREATED = 1
|
||||
COMPLETED = 10
|
||||
status = models.SmallIntegerField(default=CREATED)
|
||||
|
||||
# Did the user opt to manually manage licenses before clicking on update button?
|
||||
is_manual_license_management_upgrade_session = models.BooleanField(default=False)
|
||||
|
||||
def get_status_as_string(self) -> str:
|
||||
return {Session.CREATED: "created", Session.COMPLETED: "completed"}[self.status]
|
||||
|
||||
def get_type_as_string(self) -> str:
|
||||
return {
|
||||
Session.UPGRADE_FROM_BILLING_PAGE: "upgrade_from_billing_page",
|
||||
Session.RETRY_UPGRADE_WITH_ANOTHER_PAYMENT_METHOD: "retry_upgrade_with_another_payment_method",
|
||||
Session.FREE_TRIAL_UPGRADE_FROM_BILLING_PAGE: "free_trial_upgrade_from_billing_page",
|
||||
Session.FREE_TRIAL_UPGRADE_FROM_ONBOARDING_PAGE: "free_trial_upgrade_from_onboarding_page",
|
||||
Session.CARD_UPDATE_FROM_BILLING_PAGE: "card_update_from_billing_page",
|
||||
Session.CARD_UPDATE_FROM_UPGRADE_PAGE: "card_update_from_upgrade_page",
|
||||
}[self.type]
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
@@ -133,9 +122,8 @@ class Session(models.Model):
|
||||
|
||||
session_dict["status"] = self.get_status_as_string()
|
||||
session_dict["type"] = self.get_type_as_string()
|
||||
session_dict[
|
||||
"is_manual_license_management_upgrade_session"
|
||||
] = self.is_manual_license_management_upgrade_session
|
||||
if self.payment_intent:
|
||||
session_dict["stripe_payment_intent_id"] = self.payment_intent.stripe_payment_intent_id
|
||||
event = self.get_last_associated_event()
|
||||
if event is not None:
|
||||
session_dict["event_handler"] = event.get_event_handler_details_as_dict()
|
||||
@@ -180,15 +168,18 @@ class PaymentIntent(models.Model):
|
||||
def get_last_associated_event(self) -> Optional[Event]:
|
||||
if self.status == PaymentIntent.SUCCEEDED:
|
||||
event_type = "payment_intent.succeeded"
|
||||
# TODO: Add test for this case. Not sure how to trigger naturally.
|
||||
else: # nocoverage
|
||||
return None # nocoverage
|
||||
elif self.status == PaymentIntent.REQUIRES_PAYMENT_METHOD:
|
||||
event_type = "payment_intent.payment_failed"
|
||||
else:
|
||||
return None
|
||||
return get_last_associated_event_by_type(self, event_type)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
payment_intent_dict: Dict[str, Any] = {}
|
||||
payment_intent_dict["status"] = self.get_status_as_string()
|
||||
event = self.get_last_associated_event()
|
||||
if self.last_payment_error:
|
||||
payment_intent_dict["last_payment_error"] = self.last_payment_error
|
||||
if event is not None:
|
||||
payment_intent_dict["event_handler"] = event.get_event_handler_details_as_dict()
|
||||
return payment_intent_dict
|
||||
@@ -224,12 +215,8 @@ class CustomerPlan(models.Model):
|
||||
# billing_cycle_anchor.
|
||||
billing_cycle_anchor = models.DateTimeField()
|
||||
|
||||
BILLING_SCHEDULE_ANNUAL = 1
|
||||
BILLING_SCHEDULE_MONTHLY = 2
|
||||
BILLING_SCHEDULES = {
|
||||
BILLING_SCHEDULE_ANNUAL: "Annual",
|
||||
BILLING_SCHEDULE_MONTHLY: "Monthly",
|
||||
}
|
||||
ANNUAL = 1
|
||||
MONTHLY = 2
|
||||
billing_schedule = models.SmallIntegerField()
|
||||
|
||||
# The next date the billing system should go through ledger
|
||||
@@ -249,36 +236,24 @@ class CustomerPlan(models.Model):
|
||||
)
|
||||
end_date = models.DateTimeField(null=True)
|
||||
|
||||
INVOICING_STATUS_DONE = 1
|
||||
INVOICING_STATUS_STARTED = 2
|
||||
INVOICING_STATUS_INITIAL_INVOICE_TO_BE_SENT = 3
|
||||
DONE = 1
|
||||
STARTED = 2
|
||||
INITIAL_INVOICE_TO_BE_SENT = 3
|
||||
# This status field helps ensure any errors encountered during the
|
||||
# invoicing process do not leave our invoicing system in a broken
|
||||
# state.
|
||||
invoicing_status = models.SmallIntegerField(default=INVOICING_STATUS_DONE)
|
||||
invoicing_status = models.SmallIntegerField(default=DONE)
|
||||
|
||||
TIER_CLOUD_STANDARD = 1
|
||||
TIER_CLOUD_PLUS = 2
|
||||
# Reserved tier IDs for future use
|
||||
TIER_CLOUD_COMMUNITY = 3
|
||||
TIER_CLOUD_ENTERPRISE = 4
|
||||
|
||||
TIER_SELF_HOSTED_BASE = 100
|
||||
TIER_SELF_HOSTED_LEGACY = 101
|
||||
TIER_SELF_HOSTED_COMMUNITY = 102
|
||||
TIER_SELF_HOSTED_BUSINESS = 103
|
||||
TIER_SELF_HOSTED_PLUS = 104
|
||||
TIER_SELF_HOSTED_ENTERPRISE = 105
|
||||
STANDARD = 1
|
||||
PLUS = 2 # not available through self-serve signup
|
||||
ENTERPRISE = 10
|
||||
tier = models.SmallIntegerField()
|
||||
|
||||
ACTIVE = 1
|
||||
DOWNGRADE_AT_END_OF_CYCLE = 2
|
||||
FREE_TRIAL = 3
|
||||
SWITCH_TO_ANNUAL_AT_END_OF_CYCLE = 4
|
||||
SWITCH_PLAN_TIER_NOW = 5
|
||||
SWITCH_TO_MONTHLY_AT_END_OF_CYCLE = 6
|
||||
DOWNGRADE_AT_END_OF_FREE_TRIAL = 7
|
||||
SWITCH_PLAN_TIER_AT_PLAN_END = 8
|
||||
SWITCH_NOW_FROM_STANDARD_TO_PLUS = 5
|
||||
# "Live" plans should have a value < LIVE_STATUS_THRESHOLD.
|
||||
# There should be at most one live plan per customer.
|
||||
LIVE_STATUS_THRESHOLD = 10
|
||||
@@ -289,37 +264,19 @@ class CustomerPlan(models.Model):
|
||||
# TODO maybe override setattr to ensure billing_cycle_anchor, etc
|
||||
# are immutable.
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name} (status: {self.get_plan_status_as_text()})"
|
||||
|
||||
@staticmethod
|
||||
def name_from_tier(tier: int) -> str:
|
||||
# NOTE: Check `statement_descriptor` values after updating this.
|
||||
# Stripe has a 22 character limit on the statement descriptor length.
|
||||
# https://stripe.com/docs/payments/account/statement-descriptors
|
||||
return {
|
||||
CustomerPlan.TIER_CLOUD_STANDARD: "Zulip Cloud Standard",
|
||||
CustomerPlan.TIER_CLOUD_PLUS: "Zulip Cloud Plus",
|
||||
CustomerPlan.TIER_CLOUD_ENTERPRISE: "Zulip Enterprise",
|
||||
CustomerPlan.TIER_SELF_HOSTED_LEGACY: "Self-managed (legacy plan)",
|
||||
CustomerPlan.TIER_SELF_HOSTED_BUSINESS: "Zulip Business",
|
||||
CustomerPlan.TIER_SELF_HOSTED_COMMUNITY: "Community",
|
||||
}[tier]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.name_from_tier(self.tier)
|
||||
return {
|
||||
CustomerPlan.STANDARD: "Zulip Cloud Standard",
|
||||
CustomerPlan.PLUS: "Zulip Plus",
|
||||
CustomerPlan.ENTERPRISE: "Zulip Enterprise",
|
||||
}[self.tier]
|
||||
|
||||
def get_plan_status_as_text(self) -> str:
|
||||
return {
|
||||
self.ACTIVE: "Active",
|
||||
self.DOWNGRADE_AT_END_OF_CYCLE: "Scheduled for downgrade at end of cycle",
|
||||
self.FREE_TRIAL: "Free trial",
|
||||
self.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE: "Scheduled for switch to annual at end of cycle",
|
||||
self.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE: "Scheduled for switch to monthly at end of cycle",
|
||||
self.DOWNGRADE_AT_END_OF_FREE_TRIAL: "Scheduled for downgrade at end of free trial",
|
||||
self.SWITCH_PLAN_TIER_AT_PLAN_END: "Scheduled for switch to new plan at the end of plan",
|
||||
self.ENDED: "Ended",
|
||||
self.NEVER_STARTED: "Never started",
|
||||
}[self.status]
|
||||
@@ -330,10 +287,7 @@ class CustomerPlan(models.Model):
|
||||
return ledger_entry.licenses
|
||||
|
||||
def licenses_at_next_renewal(self) -> Optional[int]:
|
||||
if self.status in (
|
||||
CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE,
|
||||
CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL,
|
||||
):
|
||||
if self.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
|
||||
return None
|
||||
ledger_entry = LicenseLedger.objects.filter(plan=self).order_by("id").last()
|
||||
assert ledger_entry is not None
|
||||
@@ -390,16 +344,9 @@ class LicenseLedger(models.Model):
|
||||
licenses_at_next_renewal = models.IntegerField(null=True)
|
||||
|
||||
|
||||
class SponsoredPlanTypes(Enum):
|
||||
# unspecified used for cloud sponsorship requests
|
||||
UNSPECIFIED = ""
|
||||
COMMUNITY = "Community"
|
||||
BUSINESS = "Business"
|
||||
|
||||
|
||||
class ZulipSponsorshipRequest(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
||||
requested_by = models.ForeignKey(UserProfile, on_delete=CASCADE, null=True, blank=True)
|
||||
realm = models.ForeignKey(Realm, on_delete=CASCADE)
|
||||
requested_by = models.ForeignKey(UserProfile, on_delete=CASCADE)
|
||||
|
||||
org_type = models.PositiveSmallIntegerField(
|
||||
default=Realm.ORG_TYPES["unspecified"]["id"],
|
||||
@@ -410,12 +357,3 @@ class ZulipSponsorshipRequest(models.Model):
|
||||
org_website = models.URLField(max_length=MAX_ORG_URL_LENGTH, blank=True, null=True)
|
||||
|
||||
org_description = models.TextField(default="")
|
||||
expected_total_users = models.TextField(default="")
|
||||
paid_users_count = models.TextField(default="")
|
||||
paid_users_description = models.TextField(default="")
|
||||
|
||||
requested_plan = models.CharField(
|
||||
max_length=50,
|
||||
choices=[(plan.value, plan.name) for plan in SponsoredPlanTypes],
|
||||
default=SponsoredPlanTypes.UNSPECIFIED.value,
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"calculated_statement_descriptor": "ZULIP STANDARD",
|
||||
"captured": true,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -30,7 +30,6 @@
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
@@ -42,7 +41,6 @@
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
@@ -64,10 +62,9 @@
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_NORMALIZED00000000000001",
|
||||
"payment_method": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"amount_authorized": 7200,
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
@@ -75,39 +72,21 @@
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"extended_authorization": {
|
||||
"status": "disabled"
|
||||
},
|
||||
"exp_month": 3,
|
||||
"exp_year": 2033,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"incremental_authorization": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"multicapture": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"network": "visa",
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 7200,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"radar_options": {},
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKIr9qqsGMgaM-GKNpmc6LBZoWb7DJvCIVhczLJKOYXKROrzmBYTFtupIgmreHcTL1xLwXB0pQP-R7gBA",
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000001/rcpt_NORMALIZED000000000000000000001",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"calculated_statement_descriptor": "ZULIP STANDARD",
|
||||
"captured": true,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -30,7 +30,6 @@
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
@@ -42,7 +41,6 @@
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "6000",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
@@ -64,10 +62,9 @@
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_NORMALIZED00000000000002",
|
||||
"payment_method": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"payment_method": "pm_1K2OXyHSaWXyvFpKNE66Ex7T",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"amount_authorized": 36000,
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
@@ -75,39 +72,21 @@
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"extended_authorization": {
|
||||
"status": "disabled"
|
||||
},
|
||||
"exp_month": 3,
|
||||
"exp_year": 2033,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"incremental_authorization": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"multicapture": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"network": "visa",
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 36000,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"radar_options": {},
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKJT9qqsGMgbmGcE2CPg6LBYt6tPCI7CnKaxaLVq3zjSaM7atoyWnbp6xDOd4JEAbIIuFCeKskEzYWUyb",
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000002/rcpt_NORMALIZED000000000000000000002",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
@@ -147,7 +126,7 @@
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"calculated_statement_descriptor": "ZULIP STANDARD",
|
||||
"captured": true,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -156,7 +135,6 @@
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
@@ -168,7 +146,6 @@
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
@@ -190,10 +167,9 @@
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_NORMALIZED00000000000001",
|
||||
"payment_method": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"amount_authorized": 7200,
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
@@ -201,39 +177,21 @@
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"extended_authorization": {
|
||||
"status": "disabled"
|
||||
},
|
||||
"exp_month": 3,
|
||||
"exp_year": 2033,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"incremental_authorization": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"multicapture": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"network": "visa",
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 7200,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"radar_options": {},
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKJT9qqsGMgZmMGKxY746LBarTbqnnWLLGRzmrQAWL3_ig-D1uUENWBVpjxcDGfuFmMRWLxqSgEkXJuqq",
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000001/rcpt_NORMALIZED000000000000000000001",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -14,8 +13,7 @@
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": null,
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
"footer": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
@@ -28,6 +26,5 @@
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
"tax_exempt": "none"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -13,9 +12,8 @@
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
"default_payment_method": null,
|
||||
"footer": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
@@ -28,6 +26,5 @@
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
"tax_exempt": "none"
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"address": null,
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -13,9 +12,8 @@
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
"default_payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"footer": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
@@ -23,11 +21,10 @@
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 2,
|
||||
"next_invoice_sequence": 1,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
"tax_exempt": "none"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"address": null,
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
"discount": null,
|
||||
"email": "hamlet@zulip.com",
|
||||
"id": "cus_NORMALIZED0001",
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": "pm_1K2OXyHSaWXyvFpKNE66Ex7T",
|
||||
"footer": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 2,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none"
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"address": null,
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
"discount": null,
|
||||
"email": "hamlet@zulip.com",
|
||||
"id": "cus_NORMALIZED0001",
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": {
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
"last4": "4242",
|
||||
"networks": {
|
||||
"available": [
|
||||
"visa"
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"id": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "payment_method",
|
||||
"type": "card"
|
||||
},
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 1,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"address": null,
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
"discount": null,
|
||||
"email": "hamlet@zulip.com",
|
||||
"id": "cus_NORMALIZED0001",
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": {
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
"last4": "4242",
|
||||
"networks": {
|
||||
"available": [
|
||||
"visa"
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"id": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "payment_method",
|
||||
"type": "card"
|
||||
},
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 1,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"address": null,
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
"discount": null,
|
||||
"email": "hamlet@zulip.com",
|
||||
"id": "cus_NORMALIZED0001",
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": {
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
"last4": "4242",
|
||||
"networks": {
|
||||
"available": [
|
||||
"visa"
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"id": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "payment_method",
|
||||
"type": "card"
|
||||
},
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 2,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"address": null,
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
"discount": null,
|
||||
"email": "hamlet@zulip.com",
|
||||
"id": "cus_NORMALIZED0001",
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": {
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
"last4": "4242",
|
||||
"networks": {
|
||||
"available": [
|
||||
"visa"
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"id": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "payment_method",
|
||||
"type": "card"
|
||||
},
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 2,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"address": null,
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
"discount": null,
|
||||
"email": "hamlet@zulip.com",
|
||||
"id": "cus_NORMALIZED0001",
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": {
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
"last4": "4242",
|
||||
"networks": {
|
||||
"available": [
|
||||
"visa"
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"id": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "payment_method",
|
||||
"type": "card"
|
||||
},
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 2,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"address": null,
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
"discount": null,
|
||||
"email": "hamlet@zulip.com",
|
||||
"id": "cus_NORMALIZED0001",
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": {
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
"last4": "4242",
|
||||
"networks": {
|
||||
"available": [
|
||||
"visa"
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"id": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "payment_method",
|
||||
"type": "card"
|
||||
},
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 2,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
}
|
||||
@@ -5,53 +5,189 @@
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"address": null,
|
||||
"balance": 0,
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"status": null
|
||||
},
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"currency": "usd",
|
||||
"custom_fields": null,
|
||||
"customer": "cus_NORMALIZED0002",
|
||||
"customer_address": null,
|
||||
"customer_email": "hamlet@zulip.com",
|
||||
"customer_name": null,
|
||||
"customer_phone": null,
|
||||
"customer_shipping": null,
|
||||
"customer_tax_exempt": "none",
|
||||
"customer_tax_ids": [],
|
||||
"default_payment_method": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"email": "hamlet@zulip.com",
|
||||
"id": "cus_NORMALIZED0001",
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BRzhHOXVXS0dTMk5hbEl2TEhOdnM1ZUF0dloz0100yY43uPHV",
|
||||
"id": "in_NORMALIZED00000000000001",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BRzhHOXVXS0dTMk5hbEl2TEhOdnM1ZUF0dloz0100yY43uPHV/pdf",
|
||||
"last_finalization_error": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 48000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000001",
|
||||
"invoice_item": "ii_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 8000,
|
||||
"unit_amount_decimal": "8000"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -48000,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000002",
|
||||
"invoice_item": "ii_NORMALIZED00000000000002",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": -48000,
|
||||
"unit_amount_decimal": "-48000"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000001/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0001",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 1,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
},
|
||||
"previous_attributes": {
|
||||
"invoice_settings": {
|
||||
"default_payment_method": null
|
||||
}
|
||||
"period_end": 1000000000,
|
||||
"period_start": 1000000000,
|
||||
"post_payment_credit_notes_amount": 0,
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
"status_transitions": {
|
||||
"finalized_at": 1000000000,
|
||||
"marked_uncollectible_at": null,
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subtotal": 0,
|
||||
"tax": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlcWDEQaroqDjsDYC5kGA8",
|
||||
"id": "evt_1K2OXlHSaWXyvFpKIjChqmtl",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0001",
|
||||
"idempotency_key": "ec0d8ceb-bc0b-4141-9ecf-9f91d42f5a7e"
|
||||
"idempotency_key": "cae8e48c-5622-4bdf-85a2-cb06a7ed12d4"
|
||||
},
|
||||
"type": "customer.updated"
|
||||
"type": "invoice.payment_succeeded"
|
||||
}
|
||||
],
|
||||
"has_more": true,
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
"object": {
|
||||
"amount": 7200,
|
||||
"amount_capturable": 0,
|
||||
"amount_details": {
|
||||
"tip": {}
|
||||
},
|
||||
"amount_received": 7200,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
@@ -40,7 +37,7 @@
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"calculated_statement_descriptor": "ZULIP STANDARD",
|
||||
"captured": true,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -49,7 +46,6 @@
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
@@ -61,7 +57,6 @@
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
@@ -83,10 +78,9 @@
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_NORMALIZED00000000000001",
|
||||
"payment_method": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"amount_authorized": 7200,
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
@@ -94,39 +88,21 @@
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"extended_authorization": {
|
||||
"status": "disabled"
|
||||
},
|
||||
"exp_month": 3,
|
||||
"exp_year": 2033,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"incremental_authorization": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"multicapture": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"network": "visa",
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 7200,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"radar_options": {},
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKIf9qqsGMgZb20Gi7do6LBb8Nk4YXcMYuHylkNS09JAb63jzjtrknnoa60TiX-hHWdshgcroMLKDWN5c",
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000001/rcpt_NORMALIZED000000000000000000001",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
@@ -151,7 +127,7 @@
|
||||
"total_count": 1,
|
||||
"url": "/v1/charges?payment_intent=pi_NORMALIZED00000000000001"
|
||||
},
|
||||
"client_secret": "pi_NORMALIZED00000000000001_secret_TT71wq3meHmzTEd7mpZbqrDnP",
|
||||
"client_secret": "pi_NORMALIZED00000000000001_secret_fD7F9AdDLLYQt94Ii4rEflkYg",
|
||||
"confirmation_method": "automatic",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -160,14 +136,12 @@
|
||||
"id": "pi_NORMALIZED00000000000001",
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"latest_charge": "ch_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
@@ -179,12 +153,10 @@
|
||||
"next_action": null,
|
||||
"object": "payment_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"payment_method_configuration_details": null,
|
||||
"payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"mandate_options": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
@@ -192,7 +164,6 @@
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"processing": null,
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
@@ -205,13 +176,13 @@
|
||||
"transfer_group": null
|
||||
}
|
||||
},
|
||||
"id": "evt_3OIlcYDEQaroqDjs16WfiUYV",
|
||||
"id": "evt_3K2OXoHSaWXyvFpK1IgAmGCx",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 2,
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0002",
|
||||
"idempotency_key": "89f4d29d-4c41-4c1d-baf7-b33a1c7d25ff"
|
||||
"idempotency_key": "bbd6840b-375d-408a-a4b2-0353118fef83"
|
||||
},
|
||||
"type": "payment_intent.succeeded"
|
||||
},
|
||||
@@ -240,7 +211,7 @@
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"calculated_statement_descriptor": "ZULIP STANDARD",
|
||||
"captured": true,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -249,7 +220,6 @@
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
@@ -261,7 +231,6 @@
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
@@ -283,10 +252,9 @@
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_NORMALIZED00000000000001",
|
||||
"payment_method": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"amount_authorized": 7200,
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
@@ -294,39 +262,21 @@
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"extended_authorization": {
|
||||
"status": "disabled"
|
||||
},
|
||||
"exp_month": 3,
|
||||
"exp_year": 2033,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"incremental_authorization": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"multicapture": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"network": "visa",
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 7200,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"radar_options": {},
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKIf9qqsGMgaRG54HB6k6LBYbdkPPJRe4ZuuV5tR6SrHKPTGj9DFYvf8ZlOiAmw8lVKk8NDaGGFlsjRM8",
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000001/rcpt_NORMALIZED000000000000000000001",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
@@ -346,15 +296,420 @@
|
||||
"transfer_group": null
|
||||
}
|
||||
},
|
||||
"id": "evt_3OIlcYDEQaroqDjs1OA8t8hC",
|
||||
"id": "evt_3K2OXoHSaWXyvFpK1BCXNPsv",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 2,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0002",
|
||||
"idempotency_key": "bbd6840b-375d-408a-a4b2-0353118fef83"
|
||||
},
|
||||
"type": "charge.succeeded"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"address": null,
|
||||
"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,
|
||||
"default_payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"footer": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 1,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none"
|
||||
},
|
||||
"previous_attributes": {
|
||||
"invoice_settings": {
|
||||
"default_payment_method": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OXrHSaWXyvFpKTft9z8iM",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0002",
|
||||
"idempotency_key": "89f4d29d-4c41-4c1d-baf7-b33a1c7d25ff"
|
||||
"id": "req_NORMALIZED0003",
|
||||
"idempotency_key": "41855f8d-7eaf-45a3-8f4b-ebf23c527d2a"
|
||||
},
|
||||
"type": "charge.succeeded"
|
||||
"type": "customer.updated"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"application": null,
|
||||
"cancellation_reason": null,
|
||||
"client_secret": "seti_1K2OXpHSaWXyvFpKOq6F3F9K_secret_KhoAzpsEjV8G4oAeYDSFmGYMKv5BRkc",
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": null,
|
||||
"id": "seti_1K2OXpHSaWXyvFpKOq6F3F9K",
|
||||
"last_setup_error": null,
|
||||
"latest_attempt": "setatt_1K2OXpHSaWXyvFpKcauo7Bx8",
|
||||
"livemode": false,
|
||||
"mandate": null,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_action": null,
|
||||
"object": "setup_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"single_use_mandate": null,
|
||||
"status": "succeeded",
|
||||
"usage": "off_session"
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OXqHSaWXyvFpKyccpbqqf",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0004",
|
||||
"idempotency_key": "8c0e4f10-2995-45d3-9b35-aa76865e3557"
|
||||
},
|
||||
"type": "setup_intent.succeeded"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 3,
|
||||
"exp_year": 2033,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
"last4": "4242",
|
||||
"networks": {
|
||||
"available": [
|
||||
"visa"
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"id": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "payment_method",
|
||||
"type": "card"
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OXqHSaWXyvFpKUpLnWMHc",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0004",
|
||||
"idempotency_key": "8c0e4f10-2995-45d3-9b35-aa76865e3557"
|
||||
},
|
||||
"type": "payment_method.attached"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"application": null,
|
||||
"cancellation_reason": null,
|
||||
"client_secret": "seti_1K2OXpHSaWXyvFpKOq6F3F9K_secret_KhoAzpsEjV8G4oAeYDSFmGYMKv5BRkc",
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": null,
|
||||
"id": "seti_1K2OXpHSaWXyvFpKOq6F3F9K",
|
||||
"last_setup_error": null,
|
||||
"latest_attempt": "setatt_1K2OXpHSaWXyvFpKcauo7Bx8",
|
||||
"livemode": false,
|
||||
"mandate": null,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_action": null,
|
||||
"object": "setup_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"single_use_mandate": null,
|
||||
"status": "succeeded",
|
||||
"usage": "off_session"
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OXqHSaWXyvFpKZ0zBpGzN",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0004",
|
||||
"idempotency_key": "8c0e4f10-2995-45d3-9b35-aa76865e3557"
|
||||
},
|
||||
"type": "setup_intent.created"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"application": null,
|
||||
"cancellation_reason": null,
|
||||
"client_secret": "seti_1K2OXoHSaWXyvFpKLyy5ns16_secret_KhoANgFdO2YICL1Urfnax58nGUY0MeV",
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": null,
|
||||
"id": "seti_1K2OXoHSaWXyvFpKLyy5ns16",
|
||||
"last_setup_error": null,
|
||||
"latest_attempt": null,
|
||||
"livemode": false,
|
||||
"mandate": null,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_action": null,
|
||||
"object": "setup_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": null,
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"single_use_mandate": null,
|
||||
"status": "requires_payment_method",
|
||||
"usage": "off_session"
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OXoHSaWXyvFpKm8uayD4o",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0005",
|
||||
"idempotency_key": "1eca5c75-5177-4abb-94c1-4e6c39916bef"
|
||||
},
|
||||
"type": "setup_intent.created"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"amount": 7200,
|
||||
"amount_capturable": 0,
|
||||
"amount_received": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"automatic_payment_methods": null,
|
||||
"canceled_at": null,
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
"charges": {
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges?payment_intent=pi_NORMALIZED00000000000001"
|
||||
},
|
||||
"client_secret": "pi_NORMALIZED00000000000001_secret_fD7F9AdDLLYQt94Ii4rEflkYg",
|
||||
"confirmation_method": "automatic",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": "Upgrade to Zulip Cloud Standard, $12.0 x 6",
|
||||
"id": "pi_NORMALIZED00000000000001",
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_action": null,
|
||||
"object": "payment_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": null,
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "requires_payment_method",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
},
|
||||
"id": "evt_3K2OXoHSaWXyvFpK1tATyd2u",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0006",
|
||||
"idempotency_key": "d6ea8ee3-361c-451e-a3d7-e841957c3d25"
|
||||
},
|
||||
"type": "payment_intent.created"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"address": null,
|
||||
"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,
|
||||
"default_payment_method": null,
|
||||
"footer": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 1,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none"
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OXnHSaWXyvFpKsZOF5R1j",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0007",
|
||||
"idempotency_key": "d4faf9c7-d357-4870-b964-b1d993c5c058"
|
||||
},
|
||||
"type": "customer.created"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -42,77 +40,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjA502000hPybKLo?s=ap",
|
||||
"id": "in_NORMALIZED00000000000001",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjA502000hPybKLo/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000001",
|
||||
"invoice_item": "ii_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"amount_excluding_tax": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000002",
|
||||
"invoice_item": "ii_NORMALIZED00000000000002",
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -126,14 +70,57 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -143,33 +130,27 @@
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-7200"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000001/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0001",
|
||||
"number": "NORMALI-0002",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -179,15 +160,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -198,16 +170,10 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
@@ -215,18 +181,12 @@
|
||||
"previous_attributes": {
|
||||
"attempted": false,
|
||||
"auto_advance": true,
|
||||
"effective_at": null,
|
||||
"ending_balance": null,
|
||||
"hosted_invoice_url": null,
|
||||
"invoice_pdf": null,
|
||||
"next_payment_attempt": 1000000000,
|
||||
"number": null,
|
||||
"paid": false,
|
||||
"rendering": {
|
||||
"pdf": {
|
||||
"page_size": "auto"
|
||||
}
|
||||
},
|
||||
"status": "draft",
|
||||
"status_transitions": {
|
||||
"finalized_at": null,
|
||||
@@ -234,13 +194,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlcbDEQaroqDjs0RgNo7tS",
|
||||
"id": "evt_1K2OXvHSaWXyvFpKAxZLQePJ",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 2,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0003",
|
||||
"idempotency_key": "d8040535-2a29-417e-a8cd-313f534cb633"
|
||||
"id": "req_NORMALIZED0008",
|
||||
"idempotency_key": "f3d64bf5-ddf0-4933-86ee-96e9f6aa7149"
|
||||
},
|
||||
"type": "invoice.updated"
|
||||
},
|
||||
@@ -250,13 +210,11 @@
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
@@ -286,77 +244,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": null,
|
||||
"ending_balance": null,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": null,
|
||||
"id": "in_NORMALIZED00000000000001",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": null,
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000001",
|
||||
"invoice_item": "ii_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"amount_excluding_tax": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000002",
|
||||
"invoice_item": "ii_NORMALIZED00000000000002",
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -370,14 +274,57 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -387,21 +334,17 @@
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-7200"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000001/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -410,10 +353,8 @@
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -423,15 +364,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "auto"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "draft",
|
||||
@@ -442,31 +374,88 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlcaDEQaroqDjsgW8xhHxB",
|
||||
"id": "evt_1K2OXvHSaWXyvFpKykfdKZvo",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0004",
|
||||
"idempotency_key": "e1bdf299-f588-4704-ae2c-82d0debf6534"
|
||||
"id": "req_NORMALIZED0009",
|
||||
"idempotency_key": "6f6f6557-9e62-418d-ac7f-8a0a1f602c00"
|
||||
},
|
||||
"type": "invoice.created"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"date": 1000000000,
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000004",
|
||||
"invoice": "in_NORMALIZED00000000000002",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "invoiceitem",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": -7200,
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"unit_amount": -7200,
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"previous_attributes": {
|
||||
"invoice": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OXvHSaWXyvFpKRcyaG8m2",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0009",
|
||||
"idempotency_key": "6f6f6557-9e62-418d-ac7f-8a0a1f602c00"
|
||||
},
|
||||
"type": "invoiceitem.updated"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
@@ -479,14 +468,14 @@
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000001",
|
||||
"invoice": null,
|
||||
"id": "ii_NORMALIZED00000000000003",
|
||||
"invoice": "in_NORMALIZED00000000000002",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "invoiceitem",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
@@ -494,14 +483,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -514,18 +502,80 @@
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"test_clock": null,
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"previous_attributes": {
|
||||
"invoice": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlcaDEQaroqDjsCma6I5rr",
|
||||
"id": "evt_1K2OXvHSaWXyvFpKGZKUgbvc",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0005",
|
||||
"idempotency_key": "cfb4b441-f25a-4d95-a2f0-f0f950bd40fe"
|
||||
"id": "req_NORMALIZED0009",
|
||||
"idempotency_key": "6f6f6557-9e62-418d-ac7f-8a0a1f602c00"
|
||||
},
|
||||
"type": "invoiceitem.updated"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"amount": 7200,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"date": 1000000000,
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000003",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "invoiceitem",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OXuHSaWXyvFpK7gSvsu8e",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0010",
|
||||
"idempotency_key": "b60066b7-cf3e-4569-aa2e-8050562cedb4"
|
||||
},
|
||||
"type": "invoiceitem.created"
|
||||
},
|
||||
@@ -541,7 +591,7 @@
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000002",
|
||||
"id": "ii_NORMALIZED00000000000004",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -556,14 +606,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -576,18 +625,17 @@
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"test_clock": null,
|
||||
"unit_amount": -7200,
|
||||
"unit_amount_decimal": "-7200"
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlcZDEQaroqDjs6GyIO3FO",
|
||||
"id": "evt_1K2OXuHSaWXyvFpK4K7m30wK",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0006",
|
||||
"idempotency_key": "13255827-b4d3-42fe-9f18-fbcb67305a04"
|
||||
"id": "req_NORMALIZED0011",
|
||||
"idempotency_key": "cd6054f5-d831-4874-9ed9-d7b0e3b24336"
|
||||
},
|
||||
"type": "invoiceitem.created"
|
||||
},
|
||||
@@ -600,7 +648,6 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -610,9 +657,8 @@
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
"default_payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"footer": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
@@ -625,21 +671,19 @@
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
"tax_exempt": "none"
|
||||
},
|
||||
"previous_attributes": {
|
||||
"currency": null,
|
||||
"default_currency": null
|
||||
"currency": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlcZDEQaroqDjs2IZnZELZ",
|
||||
"id": "evt_1K2OXtHSaWXyvFpKaZ6mOKiF",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0006",
|
||||
"idempotency_key": "13255827-b4d3-42fe-9f18-fbcb67305a04"
|
||||
"id": "req_NORMALIZED0011",
|
||||
"idempotency_key": "cd6054f5-d831-4874-9ed9-d7b0e3b24336"
|
||||
},
|
||||
"type": "customer.updated"
|
||||
}
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -42,77 +40,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjEw0200uGqIV7uQ?s=ap",
|
||||
"id": "in_NORMALIZED00000000000001",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjEw0200uGqIV7uQ/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000001",
|
||||
"invoice_item": "ii_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"amount_excluding_tax": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000002",
|
||||
"invoice_item": "ii_NORMALIZED00000000000002",
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -126,14 +70,57 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -143,33 +130,27 @@
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-7200"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000001/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0002",
|
||||
"number": "NORMALI-0003",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -179,15 +160,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -198,28 +170,22 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlccDEQaroqDjsArfDmoTy",
|
||||
"id": "evt_1K2OXwHSaWXyvFpKr5uez7KF",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 2,
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0003",
|
||||
"idempotency_key": "d8040535-2a29-417e-a8cd-313f534cb633"
|
||||
"id": "req_NORMALIZED0008",
|
||||
"idempotency_key": "f3d64bf5-ddf0-4933-86ee-96e9f6aa7149"
|
||||
},
|
||||
"type": "invoice.payment_succeeded"
|
||||
},
|
||||
@@ -229,13 +195,11 @@
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -265,77 +229,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjA502000hPybKLo?s=ap",
|
||||
"id": "in_NORMALIZED00000000000001",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjA502000hPybKLo/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000001",
|
||||
"invoice_item": "ii_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"amount_excluding_tax": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000002",
|
||||
"invoice_item": "ii_NORMALIZED00000000000002",
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -349,14 +259,57 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -366,33 +319,27 @@
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-7200"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000001/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0002",
|
||||
"number": "NORMALI-0003",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -402,15 +349,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -421,28 +359,22 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlcbDEQaroqDjsn0Gh2ZXB",
|
||||
"id": "evt_1K2OXvHSaWXyvFpKbgniTOEt",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 1,
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0003",
|
||||
"idempotency_key": "d8040535-2a29-417e-a8cd-313f534cb633"
|
||||
"id": "req_NORMALIZED0008",
|
||||
"idempotency_key": "f3d64bf5-ddf0-4933-86ee-96e9f6aa7149"
|
||||
},
|
||||
"type": "invoice.paid"
|
||||
},
|
||||
@@ -452,13 +384,11 @@
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -488,77 +418,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjA502000hPybKLo?s=ap",
|
||||
"id": "in_NORMALIZED00000000000001",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjA502000hPybKLo/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000001",
|
||||
"invoice_item": "ii_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"amount_excluding_tax": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000002",
|
||||
"invoice_item": "ii_NORMALIZED00000000000002",
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -572,14 +448,57 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -589,33 +508,27 @@
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-7200"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000001/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0002",
|
||||
"number": "NORMALI-0003",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -625,15 +538,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -644,28 +548,22 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlcbDEQaroqDjshdrWm9Tg",
|
||||
"id": "evt_1K2OXvHSaWXyvFpKPFo4dPYw",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0003",
|
||||
"idempotency_key": "d8040535-2a29-417e-a8cd-313f534cb633"
|
||||
"id": "req_NORMALIZED0008",
|
||||
"idempotency_key": "f3d64bf5-ddf0-4933-86ee-96e9f6aa7149"
|
||||
},
|
||||
"type": "invoice.finalized"
|
||||
}
|
||||
|
||||
@@ -5,53 +5,189 @@
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"address": null,
|
||||
"balance": 0,
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"status": null
|
||||
},
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"custom_fields": null,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"customer_address": null,
|
||||
"customer_email": "hamlet@zulip.com",
|
||||
"customer_name": null,
|
||||
"customer_phone": null,
|
||||
"customer_shipping": null,
|
||||
"customer_tax_exempt": "none",
|
||||
"customer_tax_ids": [],
|
||||
"default_payment_method": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"email": "hamlet@zulip.com",
|
||||
"id": "cus_NORMALIZED0001",
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"footer": null,
|
||||
"rendering_options": null
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX/pdf",
|
||||
"last_finalization_error": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": -7200,
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0003",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 2,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none",
|
||||
"test_clock": null
|
||||
},
|
||||
"previous_attributes": {
|
||||
"invoice_settings": {
|
||||
"default_payment_method": "pm_1OIlcUDEQaroqDjsqDT70KqK"
|
||||
}
|
||||
"period_end": 1000000000,
|
||||
"period_start": 1000000000,
|
||||
"post_payment_credit_notes_amount": 0,
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
"status_transitions": {
|
||||
"finalized_at": 1000000000,
|
||||
"marked_uncollectible_at": null,
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subtotal": 0,
|
||||
"tax": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlcgDEQaroqDjsPE8wVcBq",
|
||||
"id": "evt_1K2OXwHSaWXyvFpKr5uez7KF",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0007",
|
||||
"idempotency_key": "6e41d51b-0e25-4d1f-9da2-4472878eb45f"
|
||||
"id": "req_NORMALIZED0008",
|
||||
"idempotency_key": "f3d64bf5-ddf0-4933-86ee-96e9f6aa7149"
|
||||
},
|
||||
"type": "customer.updated"
|
||||
"type": "invoice.payment_succeeded"
|
||||
}
|
||||
],
|
||||
"has_more": true,
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
"object": {
|
||||
"amount": 36000,
|
||||
"amount_capturable": 0,
|
||||
"amount_details": {
|
||||
"tip": {}
|
||||
},
|
||||
"amount_received": 36000,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
@@ -40,7 +37,7 @@
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"calculated_statement_descriptor": "ZULIP STANDARD",
|
||||
"captured": true,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -49,7 +46,6 @@
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
@@ -61,7 +57,6 @@
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "6000",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
@@ -83,10 +78,9 @@
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_NORMALIZED00000000000002",
|
||||
"payment_method": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"payment_method": "pm_1K2OXyHSaWXyvFpKNE66Ex7T",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"amount_authorized": 36000,
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
@@ -94,39 +88,21 @@
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"extended_authorization": {
|
||||
"status": "disabled"
|
||||
},
|
||||
"exp_month": 3,
|
||||
"exp_year": 2033,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"incremental_authorization": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"multicapture": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"network": "visa",
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 36000,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"radar_options": {},
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKJH9qqsGMgbQjtDZqQM6LBZPz4stEfKrXRPEFZYCyclDAo2Dez80I8lUHYTVeT16LQpU7Ou6wGCFugiR",
|
||||
"receipt_url": "https://pay.stripe.com/receipts/acct_NORMALIZED000001/ch_NORMALIZED00000000000002/rcpt_NORMALIZED000000000000000000002",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
@@ -151,7 +127,7 @@
|
||||
"total_count": 1,
|
||||
"url": "/v1/charges?payment_intent=pi_NORMALIZED00000000000002"
|
||||
},
|
||||
"client_secret": "pi_NORMALIZED00000000000002_secret_6P4eLudA3uvQYCOoNiDTkNQEI",
|
||||
"client_secret": "pi_NORMALIZED00000000000002_secret_KPTN4Jk0sR9DrnVymxUmd0o0F",
|
||||
"confirmation_method": "automatic",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -160,14 +136,12 @@
|
||||
"id": "pi_NORMALIZED00000000000002",
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"latest_charge": "ch_NORMALIZED00000000000002",
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "6000",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
@@ -179,12 +153,10 @@
|
||||
"next_action": null,
|
||||
"object": "payment_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"payment_method_configuration_details": null,
|
||||
"payment_method": "pm_1K2OXyHSaWXyvFpKNE66Ex7T",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"mandate_options": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
@@ -192,7 +164,6 @@
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"processing": null,
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
@@ -205,13 +176,13 @@
|
||||
"transfer_group": null
|
||||
}
|
||||
},
|
||||
"id": "evt_3OIlciDEQaroqDjs14Vw2Reo",
|
||||
"id": "evt_3K2OXxHSaWXyvFpK152vEHml",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 2,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0008",
|
||||
"idempotency_key": "f7ce8212-6677-40df-912b-4ac6c3765065"
|
||||
"id": "req_NORMALIZED0012",
|
||||
"idempotency_key": "5169e60e-793c-4161-9873-1ba5c523737f"
|
||||
},
|
||||
"type": "payment_intent.succeeded"
|
||||
},
|
||||
@@ -220,13 +191,112 @@
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"amount": 36000,
|
||||
"amount_captured": 36000,
|
||||
"amount_refunded": 0,
|
||||
"address": null,
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
"discount": null,
|
||||
"email": "hamlet@zulip.com",
|
||||
"id": "cus_NORMALIZED0001",
|
||||
"invoice_prefix": "NORMA01",
|
||||
"invoice_settings": {
|
||||
"custom_fields": null,
|
||||
"default_payment_method": "pm_1K2OXyHSaWXyvFpKNE66Ex7T",
|
||||
"footer": null
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip"
|
||||
},
|
||||
"name": null,
|
||||
"next_invoice_sequence": 2,
|
||||
"object": "customer",
|
||||
"phone": null,
|
||||
"preferred_locales": [],
|
||||
"shipping": null,
|
||||
"tax_exempt": "none"
|
||||
},
|
||||
"previous_attributes": {
|
||||
"invoice_settings": {
|
||||
"default_payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OY1HSaWXyvFpKNFkX6Ye6",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0013",
|
||||
"idempotency_key": "196311c6-d3ab-40d0-aefc-f770c1411cf5"
|
||||
},
|
||||
"type": "customer.updated"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"application": null,
|
||||
"application_fee": null,
|
||||
"application_fee_amount": null,
|
||||
"balance_transaction": "txn_NORMALIZED00000000000002",
|
||||
"cancellation_reason": null,
|
||||
"client_secret": "seti_1K2OXzHSaWXyvFpKzkPjM4k7_secret_KhoALa8Xt6eK1ATCOPGurJLxmo2y6xh",
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": null,
|
||||
"id": "seti_1K2OXzHSaWXyvFpKzkPjM4k7",
|
||||
"last_setup_error": null,
|
||||
"latest_attempt": "setatt_1K2OXzHSaWXyvFpKG0BonJhW",
|
||||
"livemode": false,
|
||||
"mandate": null,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"price_per_license": "6000",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_action": null,
|
||||
"object": "setup_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1K2OXyHSaWXyvFpKNE66Ex7T",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"single_use_mandate": null,
|
||||
"status": "succeeded",
|
||||
"usage": "off_session"
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OY0HSaWXyvFpKOGS3PDsC",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0014",
|
||||
"idempotency_key": "415ae25a-0c46-48f9-8299-d873f9fc51f1"
|
||||
},
|
||||
"type": "setup_intent.succeeded"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
@@ -240,28 +310,71 @@
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"captured": true,
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 3,
|
||||
"exp_year": 2033,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
"last4": "4242",
|
||||
"networks": {
|
||||
"available": [
|
||||
"visa"
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": "Upgrade to Zulip Cloud Standard, $60.0 x 6",
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
"id": "ch_NORMALIZED00000000000002",
|
||||
"invoice": null,
|
||||
"id": "pm_1K2OXyHSaWXyvFpKNE66Ex7T",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "payment_method",
|
||||
"type": "card"
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OY0HSaWXyvFpKHdeiTg2r",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0014",
|
||||
"idempotency_key": "415ae25a-0c46-48f9-8299-d873f9fc51f1"
|
||||
},
|
||||
"type": "payment_method.attached"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"application": null,
|
||||
"cancellation_reason": null,
|
||||
"client_secret": "seti_1K2OXzHSaWXyvFpKzkPjM4k7_secret_KhoALa8Xt6eK1ATCOPGurJLxmo2y6xh",
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": null,
|
||||
"id": "seti_1K2OXzHSaWXyvFpKzkPjM4k7",
|
||||
"last_setup_error": null,
|
||||
"latest_attempt": "setatt_1K2OXzHSaWXyvFpKG0BonJhW",
|
||||
"livemode": false,
|
||||
"mandate": null,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "6000",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
@@ -270,91 +383,168 @@
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"object": "charge",
|
||||
"next_action": null,
|
||||
"object": "setup_intent",
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
"risk_score": 0,
|
||||
"seller_message": "Payment complete.",
|
||||
"type": "authorized"
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_NORMALIZED00000000000002",
|
||||
"payment_method": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"payment_method_details": {
|
||||
"payment_method": "pm_1K2OXyHSaWXyvFpKNE66Ex7T",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"amount_authorized": 36000,
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"extended_authorization": {
|
||||
"status": "disabled"
|
||||
},
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"incremental_authorization": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"multicapture": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"network": "visa",
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 36000,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"radar_options": {},
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKJH9qqsGMgYqjd-cSvE6LBaZocBeNc1j6HmSljaGjK9coB5-xRAyLYhQxqOON0JTW8MYWAJtnxrRxAiL",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/ch_NORMALIZED00000000000002/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
"source_transfer": null,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"statement_descriptor_suffix": null,
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"single_use_mandate": null,
|
||||
"status": "succeeded",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
"usage": "off_session"
|
||||
}
|
||||
},
|
||||
"id": "evt_3OIlciDEQaroqDjs1zZA7DlT",
|
||||
"id": "evt_1K2OY0HSaWXyvFpKuYHMv2vM",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0008",
|
||||
"idempotency_key": "f7ce8212-6677-40df-912b-4ac6c3765065"
|
||||
"id": "req_NORMALIZED0014",
|
||||
"idempotency_key": "415ae25a-0c46-48f9-8299-d873f9fc51f1"
|
||||
},
|
||||
"type": "charge.succeeded"
|
||||
"type": "setup_intent.created"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"application": null,
|
||||
"cancellation_reason": null,
|
||||
"client_secret": "seti_1K2OXyHSaWXyvFpKvIhWI0JW_secret_KhoAlunPkSHQPGu7zyaTpT81wBUPhqc",
|
||||
"created": 1000000000,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": null,
|
||||
"id": "seti_1K2OXyHSaWXyvFpKvIhWI0JW",
|
||||
"last_setup_error": null,
|
||||
"latest_attempt": null,
|
||||
"livemode": false,
|
||||
"mandate": null,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"price_per_license": "6000",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_action": null,
|
||||
"object": "setup_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": null,
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"single_use_mandate": null,
|
||||
"status": "requires_payment_method",
|
||||
"usage": "off_session"
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OXyHSaWXyvFpK5sw7382A",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0015",
|
||||
"idempotency_key": "de8b28ae-b3f1-416b-bae0-43f9af477f33"
|
||||
},
|
||||
"type": "setup_intent.created"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"amount": 36000,
|
||||
"amount_capturable": 0,
|
||||
"amount_received": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"automatic_payment_methods": null,
|
||||
"canceled_at": null,
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
"charges": {
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges?payment_intent=pi_NORMALIZED00000000000002"
|
||||
},
|
||||
"client_secret": "pi_NORMALIZED00000000000002_secret_KPTN4Jk0sR9DrnVymxUmd0o0F",
|
||||
"confirmation_method": "automatic",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": "Upgrade to Zulip Cloud Standard, $60.0 x 6",
|
||||
"id": "pi_NORMALIZED00000000000002",
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"price_per_license": "6000",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_action": null,
|
||||
"object": "payment_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": null,
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "requires_payment_method",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
},
|
||||
"id": "evt_3K2OXxHSaWXyvFpK1GA2kN6r",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0016",
|
||||
"idempotency_key": "d8d89509-f2c5-433c-b8ca-1ae78f192ee5"
|
||||
},
|
||||
"type": "payment_intent.created"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -42,27 +40,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjE502004xw8zwol?s=ap",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjE502004xw8zwol/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8",
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -76,14 +70,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -93,26 +86,21 @@
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -36000,
|
||||
"amount_excluding_tax": -36000,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"id": "il_NORMALIZED00000000000006",
|
||||
"invoice_item": "ii_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -126,14 +114,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -143,33 +130,27 @@
|
||||
"unit_amount_decimal": "-36000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-36000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0003",
|
||||
"number": "NORMALI-0004",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -179,15 +160,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -198,28 +170,22 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlclDEQaroqDjslxJqiQFY",
|
||||
"id": "evt_1K2OY5HSaWXyvFpK1LOtVUsj",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 2,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0009",
|
||||
"idempotency_key": "5fb5bcd9-f037-402f-b67a-6f8eb89f3c11"
|
||||
"id": "req_NORMALIZED0017",
|
||||
"idempotency_key": "c3e3b8e3-19f0-4bab-865b-d2d621dd254b"
|
||||
},
|
||||
"type": "invoice.finalized"
|
||||
},
|
||||
@@ -229,13 +195,11 @@
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -265,27 +229,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjE502004xw8zwol?s=ap",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjE502004xw8zwol/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8",
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -299,14 +259,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -316,26 +275,21 @@
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -36000,
|
||||
"amount_excluding_tax": -36000,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"id": "il_NORMALIZED00000000000006",
|
||||
"invoice_item": "ii_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -349,14 +303,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -366,33 +319,27 @@
|
||||
"unit_amount_decimal": "-36000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-36000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0003",
|
||||
"number": "NORMALI-0004",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -402,15 +349,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -421,16 +359,10 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
@@ -438,18 +370,12 @@
|
||||
"previous_attributes": {
|
||||
"attempted": false,
|
||||
"auto_advance": true,
|
||||
"effective_at": null,
|
||||
"ending_balance": null,
|
||||
"hosted_invoice_url": null,
|
||||
"invoice_pdf": null,
|
||||
"next_payment_attempt": 1000000000,
|
||||
"number": null,
|
||||
"paid": false,
|
||||
"rendering": {
|
||||
"pdf": {
|
||||
"page_size": "auto"
|
||||
}
|
||||
},
|
||||
"status": "draft",
|
||||
"status_transitions": {
|
||||
"finalized_at": null,
|
||||
@@ -457,13 +383,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlclDEQaroqDjsbyHu8WF8",
|
||||
"id": "evt_1K2OY5HSaWXyvFpKAdaAGCmF",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 1,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0009",
|
||||
"idempotency_key": "5fb5bcd9-f037-402f-b67a-6f8eb89f3c11"
|
||||
"id": "req_NORMALIZED0017",
|
||||
"idempotency_key": "c3e3b8e3-19f0-4bab-865b-d2d621dd254b"
|
||||
},
|
||||
"type": "invoice.updated"
|
||||
},
|
||||
@@ -473,13 +399,11 @@
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
@@ -509,27 +433,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": null,
|
||||
"ending_balance": null,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": null,
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"invoice_pdf": null,
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -543,14 +463,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -560,26 +479,21 @@
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -36000,
|
||||
"amount_excluding_tax": -36000,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"id": "il_NORMALIZED00000000000006",
|
||||
"invoice_item": "ii_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -593,14 +507,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -610,21 +523,17 @@
|
||||
"unit_amount_decimal": "-36000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-36000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -633,10 +542,8 @@
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -646,15 +553,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "auto"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "draft",
|
||||
@@ -665,31 +563,88 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlckDEQaroqDjsUsuf3ZOp",
|
||||
"id": "evt_1K2OY4HSaWXyvFpK5WkKkAff",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0010",
|
||||
"idempotency_key": "a4d2ad76-c98a-446a-8236-ec43f0faf5fa"
|
||||
"id": "req_NORMALIZED0018",
|
||||
"idempotency_key": "f117a125-002e-4795-b38a-dd9babcf6f10"
|
||||
},
|
||||
"type": "invoice.created"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"amount": -36000,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"date": 1000000000,
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000006",
|
||||
"invoice": "in_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "invoiceitem",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": -36000,
|
||||
"unit_amount_decimal": "-36000"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"unit_amount": -36000,
|
||||
"unit_amount_decimal": "-36000"
|
||||
},
|
||||
"previous_attributes": {
|
||||
"invoice": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OY4HSaWXyvFpKtk38Nrov",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0018",
|
||||
"idempotency_key": "f117a125-002e-4795-b38a-dd9babcf6f10"
|
||||
},
|
||||
"type": "invoiceitem.updated"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
@@ -702,7 +657,70 @@
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000003",
|
||||
"id": "ii_NORMALIZED00000000000005",
|
||||
"invoice": "in_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "invoiceitem",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 6000,
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"unit_amount": 6000,
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"previous_attributes": {
|
||||
"invoice": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1K2OY4HSaWXyvFpK7ju3WXQp",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0018",
|
||||
"idempotency_key": "f117a125-002e-4795-b38a-dd9babcf6f10"
|
||||
},
|
||||
"type": "invoiceitem.updated"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"amount": 36000,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"date": 1000000000,
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000005",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -717,14 +735,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -737,18 +754,17 @@
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"test_clock": null,
|
||||
"unit_amount": 6000,
|
||||
"unit_amount_decimal": "6000"
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlckDEQaroqDjsYzRhmX8V",
|
||||
"id": "evt_1K2OY4HSaWXyvFpKxihqsHVn",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0011",
|
||||
"idempotency_key": "0c11698d-33dd-4270-81bd-05e97ff35e0e"
|
||||
"id": "req_NORMALIZED0019",
|
||||
"idempotency_key": "613148af-3c2d-4206-968d-e4ed39a3d02e"
|
||||
},
|
||||
"type": "invoiceitem.created"
|
||||
},
|
||||
@@ -764,7 +780,7 @@
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000004",
|
||||
"id": "ii_NORMALIZED00000000000006",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -779,14 +795,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -799,18 +814,17 @@
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"test_clock": null,
|
||||
"unit_amount": -36000,
|
||||
"unit_amount_decimal": "-36000"
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlcjDEQaroqDjsWfKiiWQ1",
|
||||
"id": "evt_1K2OY3HSaWXyvFpKc0mBnhSo",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0012",
|
||||
"idempotency_key": "e0683c69-8080-4a6a-b9e7-7750eba12988"
|
||||
"id": "req_NORMALIZED0020",
|
||||
"idempotency_key": "14c29b95-ba26-4fee-afc1-97ad8a540ebb"
|
||||
},
|
||||
"type": "invoiceitem.created"
|
||||
}
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -42,27 +40,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjE502004xw8zwol?s=ap",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjE502004xw8zwol/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8",
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -76,14 +70,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -93,26 +86,21 @@
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -36000,
|
||||
"amount_excluding_tax": -36000,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"id": "il_NORMALIZED00000000000006",
|
||||
"invoice_item": "ii_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -126,14 +114,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -143,33 +130,27 @@
|
||||
"unit_amount_decimal": "-36000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-36000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0003",
|
||||
"number": "NORMALI-0004",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -179,15 +160,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -198,28 +170,22 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlclDEQaroqDjsYQluU75w",
|
||||
"id": "evt_1K2OY5HSaWXyvFpK4UZiVK2B",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0009",
|
||||
"idempotency_key": "5fb5bcd9-f037-402f-b67a-6f8eb89f3c11"
|
||||
"id": "req_NORMALIZED0017",
|
||||
"idempotency_key": "c3e3b8e3-19f0-4bab-865b-d2d621dd254b"
|
||||
},
|
||||
"type": "invoice.payment_succeeded"
|
||||
},
|
||||
@@ -229,13 +195,11 @@
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -265,27 +229,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjE502004xw8zwol?s=ap",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjE502004xw8zwol/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8",
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -299,14 +259,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -316,26 +275,21 @@
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -36000,
|
||||
"amount_excluding_tax": -36000,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"id": "il_NORMALIZED00000000000006",
|
||||
"invoice_item": "ii_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -349,14 +303,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -366,33 +319,27 @@
|
||||
"unit_amount_decimal": "-36000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-36000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0003",
|
||||
"number": "NORMALI-0004",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -402,15 +349,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -421,28 +359,22 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "evt_1OIlclDEQaroqDjsWkTK3kjn",
|
||||
"id": "evt_1K2OY5HSaWXyvFpKy72Pl7GJ",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "req_NORMALIZED0009",
|
||||
"idempotency_key": "5fb5bcd9-f037-402f-b67a-6f8eb89f3c11"
|
||||
"id": "req_NORMALIZED0017",
|
||||
"idempotency_key": "c3e3b8e3-19f0-4bab-865b-d2d621dd254b"
|
||||
},
|
||||
"type": "invoice.paid"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
@@ -36,77 +34,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": null,
|
||||
"ending_balance": null,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": null,
|
||||
"id": "in_NORMALIZED00000000000001",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": null,
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000001",
|
||||
"invoice_item": "ii_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"amount_excluding_tax": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000002",
|
||||
"invoice_item": "ii_NORMALIZED00000000000002",
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -120,14 +64,57 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -137,21 +124,17 @@
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-7200"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000001/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -160,10 +143,8 @@
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -173,15 +154,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "auto"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "draft",
|
||||
@@ -192,16 +164,10 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
@@ -36,27 +34,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": null,
|
||||
"ending_balance": null,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": null,
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"invoice_pdf": null,
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -70,14 +64,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -87,26 +80,21 @@
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -36000,
|
||||
"amount_excluding_tax": -36000,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"id": "il_NORMALIZED00000000000006",
|
||||
"invoice_item": "ii_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -120,14 +108,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -137,21 +124,17 @@
|
||||
"unit_amount_decimal": "-36000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-36000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -160,10 +143,8 @@
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -173,15 +154,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "auto"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "draft",
|
||||
@@ -192,16 +164,10 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 24000,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 24000,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
@@ -36,27 +34,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": null,
|
||||
"ending_balance": null,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": null,
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"id": "in_NORMALIZED00000000000004",
|
||||
"invoice_pdf": null,
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 24000,
|
||||
"amount_excluding_tax": 24000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard - renewal",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"id": "il_NORMALIZED00000000000007",
|
||||
"invoice_item": "ii_NORMALIZED00000000000007",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -70,14 +64,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"id": "price_NORMALIZED00000000000007",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"product": "prod_NORMALIZED0007",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -87,21 +80,17 @@
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "4000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 1,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000004/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -110,10 +99,8 @@
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -123,15 +110,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "auto"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "draft",
|
||||
@@ -142,16 +120,10 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 24000,
|
||||
"subtotal_excluding_tax": 24000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 24000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 24000,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -36,77 +34,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjA502000hPybKLo?s=ap",
|
||||
"id": "in_NORMALIZED00000000000001",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjA502000hPybKLo/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000001",
|
||||
"invoice_item": "ii_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"amount_excluding_tax": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000002",
|
||||
"invoice_item": "ii_NORMALIZED00000000000002",
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -120,14 +64,57 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -137,33 +124,27 @@
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-7200"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000001/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0002",
|
||||
"number": "NORMALI-0003",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -173,15 +154,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -192,16 +164,10 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -36,27 +34,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjE502004xw8zwol?s=ap",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjE502004xw8zwol/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8",
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -70,14 +64,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -87,26 +80,21 @@
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -36000,
|
||||
"amount_excluding_tax": -36000,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"id": "il_NORMALIZED00000000000006",
|
||||
"invoice_item": "ii_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -120,14 +108,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -137,33 +124,27 @@
|
||||
"unit_amount_decimal": "-36000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-36000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0003",
|
||||
"number": "NORMALI-0004",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -173,15 +154,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -192,16 +164,10 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 24000,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 24000,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
@@ -36,27 +34,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpjd2l4VnY1aGVsY1Ztb3BRODN5dGRHbGhpNk02LDkyMDM1MjIy0200dqk7fzRz?s=ap",
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpjd2l4VnY1aGVsY1Ztb3BRODN5dGRHbGhpNk02LDkyMDM1MjIy0200dqk7fzRz/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BZ1A2VnVzRGRzR1BIS3V5YWJtejhEYVd1Z0xO0100PL5hiifd",
|
||||
"id": "in_NORMALIZED00000000000004",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BZ1A2VnVzRGRzR1BIS3V5YWJtejhEYVd1Z0xO0100PL5hiifd/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 24000,
|
||||
"amount_excluding_tax": 24000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard - renewal",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"id": "il_NORMALIZED00000000000007",
|
||||
"invoice_item": "ii_NORMALIZED00000000000007",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -70,14 +64,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"id": "price_NORMALIZED00000000000007",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"product": "prod_NORMALIZED0007",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -87,33 +80,27 @@
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "4000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 1,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000004/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": 1000000000,
|
||||
"number": "NORMALI-0004",
|
||||
"number": "NORMALI-0005",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED00000000000003",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -123,15 +110,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "open",
|
||||
@@ -142,16 +120,10 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 24000,
|
||||
"subtotal_excluding_tax": 24000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 24000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 24000,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
"data": [
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -38,77 +36,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjEx0200eb8tuh13?s=ap",
|
||||
"id": "in_NORMALIZED00000000000001",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjEx0200eb8tuh13/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000001",
|
||||
"invoice_item": "ii_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"amount_excluding_tax": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000002",
|
||||
"invoice_item": "ii_NORMALIZED00000000000002",
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -122,14 +66,57 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000004",
|
||||
"invoice_item": "ii_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -139,33 +126,27 @@
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-7200"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000001/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0002",
|
||||
"number": "NORMALI-0003",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -175,15 +156,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -194,16 +166,10 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
"data": [
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -38,27 +36,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjIx0200OMBLJkxA?s=ap",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjIx0200OMBLJkxA/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8",
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -72,14 +66,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -89,19 +82,188 @@
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -36000,
|
||||
"amount_excluding_tax": -36000,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000006",
|
||||
"invoice_item": "ii_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": -36000,
|
||||
"unit_amount_decimal": "-36000"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0004",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
"period_end": 1000000000,
|
||||
"period_start": 1000000000,
|
||||
"post_payment_credit_notes_amount": 0,
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
"status_transitions": {
|
||||
"finalized_at": 1000000000,
|
||||
"marked_uncollectible_at": null,
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subtotal": 0,
|
||||
"tax": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
},
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"status": null
|
||||
},
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_fields": null,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"customer_address": null,
|
||||
"customer_email": "hamlet@zulip.com",
|
||||
"customer_name": null,
|
||||
"customer_phone": null,
|
||||
"customer_shipping": null,
|
||||
"customer_tax_exempt": "none",
|
||||
"customer_tax_ids": [],
|
||||
"default_payment_method": null,
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX/pdf",
|
||||
"last_finalization_error": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
@@ -122,32 +284,27 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": -36000,
|
||||
"unit_amount_decimal": "-36000"
|
||||
"unit_amount": -7200,
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-36000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -162,10 +319,8 @@
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -175,15 +330,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -194,224 +340,10 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
},
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"status": null
|
||||
},
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_fields": null,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"customer_address": null,
|
||||
"customer_email": "hamlet@zulip.com",
|
||||
"customer_name": null,
|
||||
"customer_phone": null,
|
||||
"customer_shipping": null,
|
||||
"customer_tax_exempt": "none",
|
||||
"customer_tax_ids": [],
|
||||
"default_payment_method": null,
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjIx02006mE2ERoT?s=ap",
|
||||
"id": "in_NORMALIZED00000000000001",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjIx02006mE2ERoT/pdf?s=ap",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000001",
|
||||
"invoice_item": "ii_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"amount_excluding_tax": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000002",
|
||||
"invoice_item": "ii_NORMALIZED00000000000002",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": -7200,
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-7200"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000001/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0002",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
"period_end": 1000000000,
|
||||
"period_start": 1000000000,
|
||||
"post_payment_credit_notes_amount": 0,
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
"status_transitions": {
|
||||
"finalized_at": 1000000000,
|
||||
"marked_uncollectible_at": null,
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
"data": [
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 24000,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 24000,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
@@ -38,27 +36,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpjd2l4VnY1aGVsY1Ztb3BRODN5dGRHbGhpNk02LDkyMDM1MjIz02008PWTxXb7?s=ap",
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpjd2l4VnY1aGVsY1Ztb3BRODN5dGRHbGhpNk02LDkyMDM1MjIz02008PWTxXb7/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BZ1A2VnVzRGRzR1BIS3V5YWJtejhEYVd1Z0xO0100PL5hiifd",
|
||||
"id": "in_NORMALIZED00000000000004",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BZ1A2VnVzRGRzR1BIS3V5YWJtejhEYVd1Z0xO0100PL5hiifd/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 24000,
|
||||
"amount_excluding_tax": 24000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard - renewal",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"id": "il_NORMALIZED00000000000007",
|
||||
"invoice_item": "ii_NORMALIZED00000000000007",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -72,14 +66,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"id": "price_NORMALIZED00000000000007",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"product": "prod_NORMALIZED0007",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -89,33 +82,27 @@
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "4000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 1,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000004/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": 1000000000,
|
||||
"number": "NORMALI-0004",
|
||||
"number": "NORMALI-0005",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED00000000000003",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -125,15 +112,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "open",
|
||||
@@ -144,29 +122,21 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 24000,
|
||||
"subtotal_excluding_tax": 24000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 24000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 24000,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
"webhooks_delivered_at": 1000000000
|
||||
},
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
@@ -196,27 +166,23 @@
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjIz0200JbHnRP6C?s=ap",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiZG84bnRuUUtrVXRnN0pvOEFlUkJqY0ZBZFhCLDkyMDM1MjIz0200JbHnRP6C/pdf?s=ap",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8",
|
||||
"id": "in_NORMALIZED00000000000003",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9Ba2NPMVJlTGJDNXlwMEFBVU1sY01VTDNHTHNk0100ARPvAEt8/pdf",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"id": "il_NORMALIZED00000000000005",
|
||||
"invoice_item": "ii_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
@@ -230,14 +196,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -247,19 +212,188 @@
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -36000,
|
||||
"amount_excluding_tax": -36000,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000006",
|
||||
"invoice_item": "ii_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": -36000,
|
||||
"unit_amount_decimal": "-36000"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000003/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0004",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
"period_end": 1000000000,
|
||||
"period_start": 1000000000,
|
||||
"post_payment_credit_notes_amount": 0,
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
"status_transitions": {
|
||||
"finalized_at": 1000000000,
|
||||
"marked_uncollectible_at": null,
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subtotal": 0,
|
||||
"tax": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
},
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED-1",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"status": null
|
||||
},
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_fields": null,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"customer_address": null,
|
||||
"customer_email": "hamlet@zulip.com",
|
||||
"customer_name": null,
|
||||
"customer_phone": null,
|
||||
"customer_shipping": null,
|
||||
"customer_tax_exempt": "none",
|
||||
"customer_tax_ids": [],
|
||||
"default_payment_method": null,
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX",
|
||||
"id": "in_NORMALIZED00000000000002",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01cDVIU2FXWHl2RnBLLF9LaG9BNDRqUzlSeE5ZbkM2YXNhYmhHRDNCN0pTZ3E001000SGRn6zX/pdf",
|
||||
"last_finalization_error": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000003",
|
||||
"invoice_item": "ii_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
@@ -280,32 +414,27 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": -36000,
|
||||
"unit_amount_decimal": "-36000"
|
||||
"unit_amount": -7200,
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-36000"
|
||||
"type": "invoiceitem"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -320,10 +449,8 @@
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
@@ -333,15 +460,6 @@
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
@@ -352,224 +470,10 @@
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
},
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"status": null
|
||||
},
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_fields": null,
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"customer_address": null,
|
||||
"customer_email": "hamlet@zulip.com",
|
||||
"customer_name": null,
|
||||
"customer_phone": null,
|
||||
"customer_shipping": null,
|
||||
"customer_tax_exempt": "none",
|
||||
"customer_tax_ids": [],
|
||||
"default_payment_method": null,
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
"ending_balance": 0,
|
||||
"footer": null,
|
||||
"from_invoice": null,
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjIz0200ykr4PqiR?s=ap",
|
||||
"id": "in_NORMALIZED00000000000001",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/test_NORMALIZED01a3dERVFhcm9xRGpzLF9QNnpiMHNFNHp4OGxtTUNIclNiWWdZa0NQNmpqMVRJLDkyMDM1MjIz0200ykr4PqiR/pdf?s=ap",
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000001",
|
||||
"invoice_item": "ii_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
},
|
||||
{
|
||||
"amount": -7200,
|
||||
"amount_excluding_tax": -7200,
|
||||
"currency": "usd",
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "il_NORMALIZED00000000000002",
|
||||
"invoice_item": "ii_NORMALIZED00000000000002",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": -7200,
|
||||
"unit_amount_decimal": "-7200"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "-7200"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 2,
|
||||
"url": "/v1/invoices/in_NORMALIZED00000000000001/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALI-0002",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
"payment_method_types": null
|
||||
},
|
||||
"period_end": 1000000000,
|
||||
"period_start": 1000000000,
|
||||
"post_payment_credit_notes_amount": 0,
|
||||
"pre_payment_credit_notes_amount": 0,
|
||||
"quote": null,
|
||||
"receipt_number": null,
|
||||
"rendering": {
|
||||
"amount_tax_display": null,
|
||||
"pdf": {
|
||||
"page_size": "letter"
|
||||
}
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"status": "paid",
|
||||
"status_transitions": {
|
||||
"finalized_at": 1000000000,
|
||||
"marked_uncollectible_at": null,
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000002",
|
||||
"id": "ii_NORMALIZED00000000000004",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -21,14 +21,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000002",
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0004",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -41,7 +40,6 @@
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"test_clock": null,
|
||||
"unit_amount": -7200,
|
||||
"unit_amount_decimal": "-7200"
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000001",
|
||||
"id": "ii_NORMALIZED00000000000003",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "invoiceitem",
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
"end": 1000000000,
|
||||
"start": 1000000000
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
@@ -21,14 +21,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000001",
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -41,7 +40,6 @@
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"test_clock": null,
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"description": "Payment (Card ending in 4242)",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000004",
|
||||
"id": "ii_NORMALIZED00000000000006",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -21,14 +21,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000004",
|
||||
"id": "price_NORMALIZED00000000000006",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0002",
|
||||
"product": "prod_NORMALIZED0006",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -41,7 +40,6 @@
|
||||
"quantity": 1,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"test_clock": null,
|
||||
"unit_amount": -36000,
|
||||
"unit_amount_decimal": "-36000"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000003",
|
||||
"id": "ii_NORMALIZED00000000000005",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -21,14 +21,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000003",
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0001",
|
||||
"product": "prod_NORMALIZED0005",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -41,7 +40,6 @@
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"test_clock": null,
|
||||
"unit_amount": 6000,
|
||||
"unit_amount_decimal": "6000"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"description": "Zulip Cloud Standard - renewal",
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "ii_NORMALIZED00000000000005",
|
||||
"id": "ii_NORMALIZED00000000000007",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
@@ -21,14 +21,13 @@
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "price_NORMALIZED00000000000005",
|
||||
"id": "price_NORMALIZED00000000000007",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED0003",
|
||||
"product": "prod_NORMALIZED0007",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
@@ -41,7 +40,6 @@
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"test_clock": null,
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_capturable": 0,
|
||||
"amount_received": 7200,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"automatic_payment_methods": null,
|
||||
"canceled_at": null,
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
"charges": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_captured": 7200,
|
||||
"amount_refunded": 0,
|
||||
"application": null,
|
||||
"application_fee": null,
|
||||
"application_fee_amount": null,
|
||||
"balance_transaction": "txn_NORMALIZED00000000000001",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP STANDARD",
|
||||
"captured": true,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": "Upgrade to Zulip Cloud Standard, $12.0 x 6",
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
"id": "ch_NORMALIZED00000000000001",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"object": "charge",
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
"risk_score": 0,
|
||||
"seller_message": "Payment complete.",
|
||||
"type": "authorized"
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_NORMALIZED00000000000001",
|
||||
"payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 3,
|
||||
"exp_year": 2033,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"network": "visa",
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"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": null,
|
||||
"source_transfer": null,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 1,
|
||||
"url": "/v1/charges?payment_intent=pi_NORMALIZED00000000000001"
|
||||
},
|
||||
"client_secret": "pi_NORMALIZED00000000000001_secret_fD7F9AdDLLYQt94Ii4rEflkYg",
|
||||
"confirmation_method": "automatic",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": "Upgrade to Zulip Cloud Standard, $12.0 x 6",
|
||||
"id": "pi_NORMALIZED00000000000001",
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_action": null,
|
||||
"object": "payment_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1K2OXoHSaWXyvFpKhrceNhbI",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_capturable": 0,
|
||||
"amount_received": 36000,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"automatic_payment_methods": null,
|
||||
"canceled_at": null,
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
"charges": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_captured": 36000,
|
||||
"amount_refunded": 0,
|
||||
"application": null,
|
||||
"application_fee": null,
|
||||
"application_fee_amount": null,
|
||||
"balance_transaction": "txn_NORMALIZED00000000000002",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP STANDARD",
|
||||
"captured": true,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": "Upgrade to Zulip Cloud Standard, $60.0 x 6",
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
"id": "ch_NORMALIZED00000000000002",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"price_per_license": "6000",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"object": "charge",
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
"risk_score": 0,
|
||||
"seller_message": "Payment complete.",
|
||||
"type": "authorized"
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_NORMALIZED00000000000002",
|
||||
"payment_method": "pm_1K2OXyHSaWXyvFpKNE66Ex7T",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 3,
|
||||
"exp_year": 2033,
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"network": "visa",
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"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": null,
|
||||
"source_transfer": null,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 1,
|
||||
"url": "/v1/charges?payment_intent=pi_NORMALIZED00000000000002"
|
||||
},
|
||||
"client_secret": "pi_NORMALIZED00000000000002_secret_KPTN4Jk0sR9DrnVymxUmd0o0F",
|
||||
"confirmation_method": "automatic",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": "Upgrade to Zulip Cloud Standard, $60.0 x 6",
|
||||
"id": "pi_NORMALIZED00000000000002",
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"price_per_license": "6000",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_action": null,
|
||||
"object": "payment_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1K2OXyHSaWXyvFpKNE66Ex7T",
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
},
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_capturable": 0,
|
||||
"amount_details": {
|
||||
"tip": {}
|
||||
},
|
||||
"amount_received": 7200,
|
||||
"amount_received": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"automatic_payment_methods": null,
|
||||
@@ -12,140 +9,13 @@
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
"charges": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_captured": 7200,
|
||||
"amount_refunded": 0,
|
||||
"application": null,
|
||||
"application_fee": null,
|
||||
"application_fee_amount": null,
|
||||
"balance_transaction": "txn_NORMALIZED00000000000001",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"captured": true,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": "Upgrade to Zulip Cloud Standard, $12.0 x 6",
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
"id": "ch_NORMALIZED00000000000001",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"object": "charge",
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
"risk_score": 0,
|
||||
"seller_message": "Payment complete.",
|
||||
"type": "authorized"
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_NORMALIZED00000000000001",
|
||||
"payment_method": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"amount_authorized": 7200,
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"extended_authorization": {
|
||||
"status": "disabled"
|
||||
},
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"incremental_authorization": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"multicapture": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"network": "visa",
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 7200,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"radar_options": {},
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKIf9qqsGMgYln1CvRQg6LBas64aXppcCqDXm8lE0Sbd6BM7omStVTXKhathHXWOU3wC8kyUuXRRePlvZ",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/ch_NORMALIZED00000000000001/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
"source_transfer": null,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
],
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 1,
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges?payment_intent=pi_NORMALIZED00000000000001"
|
||||
},
|
||||
"client_secret": "pi_NORMALIZED00000000000001_secret_TT71wq3meHmzTEd7mpZbqrDnP",
|
||||
"client_secret": "pi_NORMALIZED00000000000001_secret_fD7F9AdDLLYQt94Ii4rEflkYg",
|
||||
"confirmation_method": "automatic",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -154,14 +24,12 @@
|
||||
"id": "pi_NORMALIZED00000000000001",
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"latest_charge": "ch_NORMALIZED00000000000001",
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "1200",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
@@ -173,12 +41,10 @@
|
||||
"next_action": null,
|
||||
"object": "payment_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1OIlcUDEQaroqDjsqDT70KqK",
|
||||
"payment_method_configuration_details": null,
|
||||
"payment_method": null,
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"mandate_options": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
@@ -186,7 +52,6 @@
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"processing": null,
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
@@ -194,7 +59,7 @@
|
||||
"source": null,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"status": "requires_payment_method",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_capturable": 0,
|
||||
"amount_details": {
|
||||
"tip": {}
|
||||
},
|
||||
"amount_received": 36000,
|
||||
"amount_received": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"automatic_payment_methods": null,
|
||||
@@ -12,140 +9,13 @@
|
||||
"cancellation_reason": null,
|
||||
"capture_method": "automatic",
|
||||
"charges": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_captured": 36000,
|
||||
"amount_refunded": 0,
|
||||
"application": null,
|
||||
"application_fee": null,
|
||||
"application_fee_amount": null,
|
||||
"balance_transaction": "txn_NORMALIZED00000000000002",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"captured": true,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"customer": "cus_NORMALIZED0001",
|
||||
"description": "Upgrade to Zulip Cloud Standard, $60.0 x 6",
|
||||
"destination": null,
|
||||
"dispute": null,
|
||||
"disputed": false,
|
||||
"failure_balance_transaction": null,
|
||||
"failure_code": null,
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
"id": "ch_NORMALIZED00000000000002",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "6000",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
"seat_count": "6",
|
||||
"type": "upgrade",
|
||||
"user_email": "hamlet@zulip.com",
|
||||
"user_id": "10"
|
||||
},
|
||||
"object": "charge",
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
"risk_score": 0,
|
||||
"seller_message": "Payment complete.",
|
||||
"type": "authorized"
|
||||
},
|
||||
"paid": true,
|
||||
"payment_intent": "pi_NORMALIZED00000000000002",
|
||||
"payment_method": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"payment_method_details": {
|
||||
"card": {
|
||||
"amount_authorized": 36000,
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": "pass"
|
||||
},
|
||||
"country": "US",
|
||||
"exp_month": 12,
|
||||
"exp_year": 2024,
|
||||
"extended_authorization": {
|
||||
"status": "disabled"
|
||||
},
|
||||
"fingerprint": "NORMALIZED000001",
|
||||
"funding": "credit",
|
||||
"incremental_authorization": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"installments": null,
|
||||
"last4": "4242",
|
||||
"mandate": null,
|
||||
"multicapture": {
|
||||
"status": "unavailable"
|
||||
},
|
||||
"network": "visa",
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 36000,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
"type": "card"
|
||||
},
|
||||
"radar_options": {},
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xN3ZUa3dERVFhcm9xRGpzKJD9qqsGMgahoOxs2tY6LBYE_7mVLMS2-_xQ_C_j9KSayaWsbJ6jjdpSQjsrQImBFFAZjT-6A-PbNsDv",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/ch_NORMALIZED00000000000002/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
"source_transfer": null,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
],
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 1,
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges?payment_intent=pi_NORMALIZED00000000000002"
|
||||
},
|
||||
"client_secret": "pi_NORMALIZED00000000000002_secret_6P4eLudA3uvQYCOoNiDTkNQEI",
|
||||
"client_secret": "pi_NORMALIZED00000000000002_secret_KPTN4Jk0sR9DrnVymxUmd0o0F",
|
||||
"confirmation_method": "automatic",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -154,14 +24,12 @@
|
||||
"id": "pi_NORMALIZED00000000000002",
|
||||
"invoice": null,
|
||||
"last_payment_error": null,
|
||||
"latest_charge": "ch_NORMALIZED00000000000002",
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_modality": "charge_automatically",
|
||||
"billing_schedule": "1",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"plan_tier": "1",
|
||||
"price_per_license": "6000",
|
||||
"realm_id": "1",
|
||||
"realm_str": "zulip",
|
||||
@@ -173,12 +41,10 @@
|
||||
"next_action": null,
|
||||
"object": "payment_intent",
|
||||
"on_behalf_of": null,
|
||||
"payment_method": "pm_1OIlceDEQaroqDjshc2Y2l9b",
|
||||
"payment_method_configuration_details": null,
|
||||
"payment_method": null,
|
||||
"payment_method_options": {
|
||||
"card": {
|
||||
"installments": null,
|
||||
"mandate_options": null,
|
||||
"network": null,
|
||||
"request_three_d_secure": "automatic"
|
||||
}
|
||||
@@ -186,7 +52,6 @@
|
||||
"payment_method_types": [
|
||||
"card"
|
||||
],
|
||||
"processing": null,
|
||||
"receipt_email": "hamlet@zulip.com",
|
||||
"review": null,
|
||||
"setup_future_usage": null,
|
||||
@@ -194,7 +59,7 @@
|
||||
"source": null,
|
||||
"statement_descriptor": "Zulip Cloud Standard",
|
||||
"statement_descriptor_suffix": null,
|
||||
"status": "succeeded",
|
||||
"status": "requires_payment_method",
|
||||
"transfer_data": null,
|
||||
"transfer_group": null
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user