Compare commits

..

185 Commits
8.1 ... 7.x

Author SHA1 Message Date
Tim Abbott
c4a826461b i18n: Update translation data from Transifex. 2023-12-15 13:29:19 -08:00
Alex Vandiver
e37a734bd7 newrelic: ids are uuids, not integers.
The previous fixtures were not generated from New Relic directly, so
incorrectly implied that `id` was an integer.  See the examples for
`issueId` in their documentation[^1].

[^1]: https://docs.newrelic.com/docs/alerts-applied-intelligence/notifications/message-templates/
2023-11-17 08:58:22 -08:00
Alex Vandiver
b7059807ac openapi: Use SuccessDescription in new get-stream-email-address.
This allows it to inherit the required `description` field.  This is
necessary for 7.x but not main because 6e119842bd was backported
across #26509.
2023-11-16 15:24:22 -05:00
Alex Vandiver
7fb252a27c rest-endpoints: Add new /api/get-stream-email-address endpoint.
(cherry picked from commit 1af1a6d7e9)
2023-11-16 20:18:14 +00:00
Alex Vandiver
6c4791bb2f version: Update version after 7.5 release. 2023-11-16 18:57:10 +00:00
Alex Vandiver
244b499200 subscription_info: Fix typo caught by codespell. 2023-11-16 18:55:44 +00:00
Alex Vandiver
09ccdc301b Release Zulip Server 7.5. 2023-11-16 17:00:11 +00:00
Sahil Batra
ee6ab3d15b streams: Send stream deletion events on unsubscribing users.
This commit adds code to send stream deletion events when
unsubscribing non-admin users from private streams and
when unsubscribing guests from public streams since
non-admins cannot access unsubscribed private streams
and guests cannot access unsubscribed public streams.
2023-11-16 16:38:47 +00:00
Sahil Batra
6336322d2f CVE-2023-47642: Invalid metadata access for formerly subscribed streams.
It was discovered by the Zulip development team that active users who
had previously been subscribed to a stream incorrectly continued being
able to use the Zulip API to access metadata for that stream. As a
result, users who had been removed from a stream, but still had an
account in the organization, could still view metadata for that
stream (including the stream name, description, settings, and an email
address used to send emails into the stream via the incoming email
integration). This potentially allowed users to see changes to a
stream’s metadata after they had lost access to the stream.

This bug was present in all Zulip releases prior to today's Zulip
Server 7.5.
2023-11-16 16:38:30 +00:00
Sahil Batra
6e119842bd streams: Add API endpoint to get stream email.
This commit adds new API endpoint to get stream email which is
used by the web-app as well to get the email when a user tries
to open the stream email modal.

The stream email is returned only to the users who have access
to it. Specifically for private streams only subscribed users
have access to its email. And for public streams, all non-guest
users and only subscribed guests have access to its email.
All users can access email of web-public streams.
2023-11-16 16:28:14 +00:00
Sahil Batra
0a3800332f streams: Remove "email_address" field from Subscription objects.
This commit removes "email_address" field from Subscription objects
and we would instead a new endpoint in next commit to get email
address for stream with proper access check.

This change also fixes the bug where we would include email address
for the unsubscribed private stream as well when user did not have
permission to send message to the stream, and having email allowed
the unsubscribed user to send message to the stream.

Note that the unsubscribed user can still send message to the stream
if the user had noted down the email before being unsubscribed
and the stream token is not changed after unsubscribing the user.
2023-11-16 11:11:26 -05:00
Sahil Batra
9636362cbd events: Fix applying stream creation events in apply_event.
There was a bug in apply_event code where only a stream which
is not private is added to the "never_subscribed" data after
a stream creation event. Instead, it should be added to the
"never_subscribed" data irrespective of permission policy of
the stream as we already send stream creation events only to
those users who can access the stream. Due to the current
bug, private streams were not being added to "never_subscribed"
data in apply_event for admins as well. This commit fixes it
and also makes sure the "never_subscribed" list is sorted
which was not done before and was also a bug.

The bugs mentioned above were unnoticed as the tests did not
cover these cases and this commit also adds tests for those
cases.
2023-11-16 11:11:26 -05:00
Sahil Batra
c90fd388c8 register: Include web-public streams in "streams" field of response.
The "streams" field in "/register" response did not include web-public
streams for non-admin users but the data for those are eventually
included in the subscriptions data sent using "subscriptions",
"unsubscribed" and "never_subscribed" fields.

This commit adds code to include the web-public streams in "streams"
field as well as everyone can access those and will make the "streams"
data complete.
2023-11-16 11:11:26 -05:00
Alex Vandiver
edf1de5416 i18n: Update translation data from Transifex. 2023-11-15 22:11:06 +00:00
Karl Stolley
a4c2f27bd5 tests: Disable flaky test_edit_bot_form test. 2023-11-15 10:50:00 -08:00
Anders Kaseorg
019d922917 requirements: Upgrade Python requirements.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 7807bff526)
2023-11-15 10:13:24 -08:00
Anders Kaseorg
64cb7b5bed rate_limiter: Fix PIE790 Unnecessary pass statement.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit f338c3af07)
2023-11-15 10:13:24 -08:00
Anders Kaseorg
41a6511924 typos: Fix typos caught by typos.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 4cb2eded68)
2023-11-15 10:13:24 -08:00
Anders Kaseorg
3a2a217fa0 codespell: Fix typos caught by codespell.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 7b4a74cc4d)
2023-11-15 10:13:24 -08:00
Anders Kaseorg
8e1ebede1b ruff: Fix C416 Unnecessary list comprehension.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit dd7b09d71a)
2023-11-15 10:13:24 -08:00
Alex Vandiver
a7b8e4795d backup: Only pass --host and --port if non-empty.
This works around the `/usr/bin/pg_dump` failure described in the
previous commit.  Since we are now calling the appropriately-versioned
`pg_dump` binary directly, it is no longer "necessary", but is added
as a defense-in-depth.
2023-11-14 12:11:40 -08:00
Alex Vandiver
9167b3efd7 backup: Call the pg_dump binary whose version we are running.
`/usr/bin/pg_dump` on Ubuntu and Debian is actually a tool which
attempts to choose which `pg_dump` binary from all of the
`postgresql-client-*` packages that are installed to run.  However,
its logic is confused by passing empty `--host` and `--port` options
-- instead of looking at the running server instance on the server, it
instead assumes some remote host and chooses the highest versioned
`pg_dump` which is installed.

Because Zulip writes binary database backups, they are sensitive to
the version of the client `pg_dump` binary is used -- and the output
may not be backwards compatible.  Using a PostgreSQL 16 `pg_dump`
writes archive format 1.15, which cannot be read by a PostgreSQL 15
`pg_restore`.

Zulip does not currently support PostgreSQL 16 as a server.  This
means that backups on servers with `postgresql-client-16` installed
did not successfully round-trip Zulip backups -- their backups are
written using PostgreSQL 16's client, and the `pg_restore` chosen on
restore was correctly chosen as the one whose version matched the
server (PostgreSQL 15 or below), and thus did not understand the new
archive format.

Existing `./manage.py backups` taken since `postgresql-client-16` were
installed are thus not directly usable by the `restore-backup` script.
They are not useless, however, since they can theoretically be
converted into a format readable by PostgreSQL 15 -- by importing into
a PostgreSQL 16 instance, and re-dumping with a PostgreSQL 15
`pg_dump`.

Fix this issue by hard-coding path to the binary whose version matches
the version of the server we are connected to.  This may theoretically
fail if we are connected to a remote PostgreSQL instance and we do not
have a `postgresql-client` package locally installed which matches the
remote PostgreSQL server's version.  However, choosing a matching
version is the only way to ensure that it will be able to be imported
cleanly -- and it is preferable that we fail the backup process rather
than write backups that we cannot easily restore from.

Fixes: #27160.
2023-11-14 12:11:40 -08:00
Alex Vandiver
e84c289c0d backup: Use simpler api for server version. 2023-11-14 12:11:40 -08:00
David Rosa
5a9452f3e6 help: Document unarchive_stream management command.
- Adds "Unarchiving archived streams" section to "Archive a stream".
- Adds `./manage.py unarchive_stream` to contributor docs.
2023-10-02 12:52:25 -07:00
David Rosa
d68b27e39a help: Update "Archive a stream".
- Updates page to follow current help center documentation patterns.
2023-10-02 12:52:25 -07:00
David Rosa
60334f7ccf management: Rename command reactivate_stream -> unarchive_stream. 2023-10-02 12:52:25 -07:00
Alex Vandiver
acaf5b835c realm: Differentiate reserved realms from in-use realms.
Fixes: #23896.
2023-10-02 12:52:25 -07:00
Mateusz Mandera
6ef745675c ldap: Improve doc on AUTH_LDAP_ADVANCED_REALM_ACCESS_CONTROL.
This fixes the explanation of the setting's syntax to be more precise
(which doesn't mean "easily understandable" - because the setting is
a bit tricky) as well as an example to illustrate it.
2023-10-02 12:52:25 -07:00
Alex Vandiver
be179b2b6b delete_old_unclaimed_attachments: Update docs on default max age.
42f1cb3444 updated the default up, from 1 week to 5 weeks, but did
not adjust the documentation.
2023-10-02 12:52:25 -07:00
Tim Abbott
79945622bc i18n: Fix default language for users created via API/LDAP.
This fixes a regression introduced in
9954db4b59, where the realm's default
language would be ignored for users created via API/LDAP/SAML,
resulting in all such users having English as their default language.

The API/LDAP/SAML account creation code paths don't have a request,
and thus cannot pull default language from the user's browser.

We have the `realm.default_language` field intended for this use case,
but it was not being passed through the system.

Rather than pass `realm.default_language` through from each caller, we
make the low-level user creation code set this field, as that seems
more robust to the creation of future callers.
2023-10-02 12:52:25 -07:00
Mateusz Mandera
092ecbacc6 i18n: Tweak args/kwargs for get_default_language_for_new_user.
Making request a mandatory kwarg avoids confusion about the meaning of
parameters, especially with `request` acquiring the ability to be None
in the upcoming next commit.
2023-10-02 12:52:25 -07:00
Tim Abbott
a802f0f339 version: Update version after 7.4 release. 2023-09-15 18:02:00 -07:00
Tim Abbott
d5b8d4b17d Release Zulip Server 7.4. 2023-09-15 17:27:53 -07:00
Tim Abbott
4de73f823c i18n: Update translation data from Transifex. 2023-09-15 17:20:41 -07:00
Anders Kaseorg
39a908d350 ci: Restore commented clean_unused_caches.py invocation.
The comment logic doesn’t make sense.  Every build gets to write to
the caches; some builds do in fact add new items, and without
clean_unused_caches.py there’s no way for them to remove items.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 124c5d02e5)
2023-09-15 13:12:36 -07:00
Anders Kaseorg
94af282460 requirements: Upgrade Python requirements.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 9a53baea93)
2023-09-15 12:22:42 -07:00
Anders Kaseorg
547ac31ee6 requirements: Upgrade Python requirements.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 1905df2342)
2023-09-15 12:21:28 -07:00
Anders Kaseorg
7c50e1a40e ruff: Fix PIE808 Unnecessary start argument in range.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 81bd63cb46)
2023-09-15 12:21:28 -07:00
Alex Vandiver
e2dcea1664 requirements: Use greenlet>=3.0.0a1 to fix uwsgi segfault.
Pull in the fix for python-greenlet/greenlet#330.

(cherry picked from commit 2e760f18ac)
2023-09-15 12:12:34 -07:00
Tim Abbott
839c86ead5 css: Fix backporting error of back-to-login button.
In 7.x, this was invalid syntax with our postcss-preset-env configuration.

See the following for context:

https://chat.zulip.org/#narrow/stream/49-development-help/topic/webpack.20exception.20running.207.2Ex/near/1641309
https://chat.zulip.org/#narrow/stream/2-general/topic/CSS.20nesting.20syntax/near/1528649
2023-09-15 12:04:38 -07:00
Alex Vandiver
48995f8218 ci: Temporarily upgrade postgresql-client-common before upgrading.
This works around a failure in the current postgresql-client-common
and postgresql-client-15 packages; it exists primarily to improve
the signal on our CI builds, as the failure is a real failure caused
by the package upgrade process.
2023-09-15 10:22:55 -07:00
Daniil Fadeev
276830b275 left_sidebar: Fix the layout for filter names that are long. 2023-09-15 10:22:55 -07:00
Alex Vandiver
f43736bef5 nginx: Do not forward X-amz-cf-id header to S3.
All `X-amz-*` headers must be included in the signed request to S3;
since Django did not take those headers into account (it constructed a
request from scratch, while nginx's request inherits them from the
end-user's request), the proxied request fails to be signed correctly.

Strip off the `X-amz-cf-id` header added by CloudFront.  While we
would ideally strip off all `X-amz-*` headers, this requires a
third-party module[^1].

[^1]: https://github.com/openresty/headers-more-nginx-module#more_clear_input_headers
2023-09-15 10:22:55 -07:00
Karl Stolley
49d7830886 compose: Render Mac-specific Cmd, Return at compose bottom. 2023-09-15 10:22:55 -07:00
Alex Vandiver
761dae7571 nginx: Suppress proxy warnings when the proxy itself sent the request.
This is common in cases where the reverse proxy itself is making
health-check requests to the Zulip server; these requests have no
X-Forwarded-* headers, so would normally hit the error case of
"request through the proxy, but no X-Forwarded-Proto header".

Add an additional special-case for when the request's originating IP
address is resolved to be the reverse proxy itself; in these cases,
HTTP requests with no X-Forwarded-Proto are acceptable.
2023-09-15 10:22:55 -07:00
Mateusz Mandera
d9c94944e0 docs: Add a warning about restarting individual services.
restart-server is almost always the way to go, so we can be clearer
about it.
2023-09-15 10:22:55 -07:00
Anders Kaseorg
3780ee2fc6 middleware: Fix exception logging format on JSON views.
Previously (with ERROR_REPORTING = True), we’d stuff the entire
traceback of the initial exception into the subject line of an error
email, and then also send a separate email for the JSON 500 response.
Instead, log one error with the standard Django format.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-15 10:22:55 -07:00
Alex Vandiver
ff520a3d52 requirements: Downgrade orjson to 3.9.2.
orjson 3.9.3 introduced a crash (ijl/orjson#415) -- revert to the last
version before the bug.

(cherry picked from commit 2612a3b657)
2023-08-31 19:06:29 +00:00
Alex Vandiver
170f8dabd4 version: Update version after 7.3 release. 2023-08-25 18:19:21 +00:00
Alex Vandiver
b85046eb08 Release Zulip Server 7.3. 2023-08-25 17:57:18 +00:00
Mateusz Mandera
ee9a450f4e bulk_access_messages_expect_usermessage: Fix function name and comments.
The name and docstring were just wrong, having a UserMessage row isn't
sufficient for having message access and is actually only relevant in a
private stream with private history. The function is only used in a
single place anyway, in bulk_access_messages.

The comment mentioning this function in handle_remove_push_notification
can be tweaked to just not mention any function specifically and just
say why we're not checking message access.
2023-08-25 17:57:09 +00:00
Mateusz Mandera
26f0695b94 delete_in_topic: Add comment explaining the access logic. 2023-08-25 17:57:09 +00:00
Mateusz Mandera
a30cd12433 CVE-2023-32678: Prevent unauthorized editing/deletion in priv streams.
Users who used to be subscribed to a private stream and have been
removed from it since retain the ability to edit messages/topics, and
delete messages that they used to have access to, if other relevant
organization permissions allow these actions. For example, a user may be
able to edit or delete their old messages they posted in such a private
stream. An administrator will be able to delete old messages (that they
had access to) from the private stream.

We fix this by fixing the logic in has_message_access (which lies at the
core of our message access checks - access_message() and
bulk_access_messages())
to not rely on only a UserMessage row for checking access but also
verify stream type and subscription status.
2023-08-25 17:56:57 +00:00
Anders Kaseorg
ea0b8cc011 requirements: Fix transitively replaced two_factor migrations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 4300ec3293)
2023-08-24 21:22:12 +00:00
Anders Kaseorg
6b016eb5bf check-database-compatibility: Ignore twofactor typo.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit ead7b9177a)
2023-08-24 21:21:38 +00:00
Satyam Bansal
35b0b2fb6a integrations: Add support for "Test plugin" in Sentry integration.
Previously, if a user tried to create a webhook using the Webhooks
plugin in Sentry and used the "Test plugin" to test the webhook,
the server would send a 500 error, even though the integration
worked perfectly. This led users to believe that the integration
was not working.

Fixes #26173.

(cherry picked from commit eb8714c9dc)
2023-08-23 03:06:07 +00:00
Satyam Bansal
05c2269571 integrations: Add Raven SDK test to Sentry Integration.
(cherry picked from commit 6898667fa4)
2023-08-23 03:06:07 +00:00
Satyam Bansal
d3048b8791 integrations: Rename fixture in Sentry integration.
This is done to clarify from where this fixture is coming from; as there
are two documented ways to test the integration.

(cherry picked from commit fdc14ee3f0)
2023-08-23 03:06:07 +00:00
Zixuan James Li
ec8a284ad5 webhooks: Use 200 status code for unknown events.
Because the third party might not be expecting a 400 from our
webhooks, we now instead use 200 status code for unknown events,
while sending back the error to Sentry. Because it is no longer an error
response, the response type should now be "success".

Fixes #24721.

(cherry picked from commit 84723654c8)
2023-08-23 03:06:07 +00:00
Alex Vandiver
61b5577cf4 sentry: Reduce http timeout.
This helps reduce the impact on busy uwsgi processes in case there are
slow timeout failures of Sentry servers.  The p99 is less than 300ms,
and p99.9 per day peaks at around 1s, so this will not affect more
than .1% of requests in normal operation.

This is not a complete solution (see #26229); it is merely stop-gap
mitigation.

(cherry picked from commit a076d49be7)
2023-08-23 03:06:07 +00:00
Lauryn Menard
21c5ea1444 sentry-webhook: Revise documentation page to be clearer.
(cherry picked from commit 3d8090a116)
2023-08-23 02:48:43 +00:00
Satyam Bansal
421ce4ffe0 integrations: Add support for sample events in Sentry Integration.
Fixes #25778.

(cherry picked from commit 16563a3217)
2023-08-23 02:29:07 +00:00
Satyam Bansal
792748ae63 integrations: Add exception fixture for Vue in Sentry Integration.
(cherry picked from commit 580d8c4dfe)
2023-08-23 02:29:07 +00:00
Satyam Bansal
1252fbe434 integrations: Check for Raven SDK only on python in Sentry Integration.
Fixes part of #25778.

(cherry picked from commit 3bdb806fba)
2023-08-23 02:29:07 +00:00
Satyam Bansal
d026f35c5b integrations: Add support for Rails backend in Sentry Integration.
(cherry picked from commit 142e455d81)
2023-08-23 02:29:07 +00:00
Satyam Bansal
c7839ff084 integrations: Update documentation for Sentry Integration.
(cherry picked from commit 9e793c37e6)
2023-08-23 02:29:07 +00:00
Greg Price
5b4f5a89fb docs: Consistently say "18 months" on compatibility, no specific version numbers.
Previously I've wanted to have this page spell out the concrete
version number that our clients support, rather than the policy we
use for determining that version number, because that's the sort of
question that I feel like as a user I'd want a straight answer to
and would be annoyed if I couldn't get one.

But as the text stands, it's come to look more like it's the policy
(something that's heavyweight to change) than like the value that
the policy currently happens to work out to.  Also, because this page
is kind of chaotically organized (and fixing that is a bigger yak
than I want to shave right now), it repeats the 18-month rule in
three separate places and the current value (version 4.0) is in
a fourth separate place, so it looks internally inconsistent.

Let's therefore take a different tack: like in those other three
spots on this page, state just the policy instead of the value it
currently works out to; but also add a link to help the reader
pin down for themselves what value that does work out to.

This also means we no longer need to update this page as old releases
age and that value advances.

Also fix a typo, and cut the reference to working degraded on
older releases.  Starting earlier this year we finally started
hard-refusing such connections:
  https://github.com/zulip/zulip-mobile/issues/5102
  https://github.com/zulip/zulip-mobile/pull/5633
(which was because there were some swathes of compatibility code
that we could only cut if we completely broke the handling of
ancient servers, and so we preferred to have the app communicate
that break clearly up front.)

(cherry picked from commit bb6fe0385e)
2023-08-23 02:29:07 +00:00
Anders Kaseorg
f54f07f6cb requirements: Upgrade Python requirements.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 113ac6eb98)
2023-08-23 02:29:07 +00:00
Anders Kaseorg
f1595cf7db requirements: Upgrade Python requirements.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit e32366638a)
2023-08-23 02:29:07 +00:00
Anders Kaseorg
e953ff71e4 tools: Use subprocess.check_call where appropriate.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 041dcdfbad)
2023-08-23 02:29:07 +00:00
Anders Kaseorg
47e2b07316 ruff: Fix PLW1510 subprocess.run without explicit check argument.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit c43629a222)
2023-08-23 02:29:07 +00:00
Anders Kaseorg
4ac849d006 ruff: Appease SIM118 "class" not in uncle.keys().
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 36dde99308)
2023-08-23 02:29:07 +00:00
Anders Kaseorg
43b09fd89d ruff: Fix PYI032 Prefer object for the second parameter to __eq__.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit ec00c2970f)
2023-08-23 02:29:07 +00:00
Anders Kaseorg
8a1704f765 ruff: Fix E721 Do not compare types, use isinstance().
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 53e8c0c497)
2023-08-23 02:29:07 +00:00
Anders Kaseorg
96be7b72c2 ruff: Collapse short multi-line import statements.
isort did this by default, though it’s unclear whether that was
intended; see https://github.com/astral-sh/ruff/issues/4153.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 733083c65d)
2023-08-23 02:29:07 +00:00
Anders Kaseorg
99b37de89f ruff: Fix UP032 Use f-string instead of format call.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit e932e2ce52)
2023-08-23 02:29:07 +00:00
Anders Kaseorg
323223db4c ruff: Fix SIM118 Use k not in d instead of k not in d.keys().
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 74d6d76046)
2023-08-23 02:29:07 +00:00
Anders Kaseorg
91e5ef39eb ruff: Fix PLR1714 Consider merging multiple comparisons.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 2ae285af7c)
2023-08-23 02:29:05 +00:00
Anders Kaseorg
b9aa772885 ruff: Fix RUF015 Prefer next(...) over single element slice.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 3b09197fdf)
2023-08-23 02:28:06 +00:00
Anders Kaseorg
a0ce536fa4 tests: Remove compatibility code for Python < 3.5.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 4e645c8ff9)
2023-08-23 02:28:06 +00:00
Anders Kaseorg
3325f2ef06 ruff: Fix UP032 Use f-string instead of format call.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit bca5564c1e)
2023-08-23 02:28:06 +00:00
Lauryn Menard
920c73a64e markdown-processor: Update insertion_index check for multiple classes.
Updates find_proper_insertion_index to check for the inline image
classes as matching at least one of the classes in the element's
attrib["class"] so that cases where an inline preview image has
multiple classes, like YouTube video previews, will have the
correct insertion index.

Fixes #26186.

(cherry picked from commit d84fd73db4)
2023-08-23 01:47:45 +00:00
Aman Agrawal
9c272d6ca7 portico_signin: Fix back to login button alignment.
(cherry picked from commit eda91378ec)
2023-08-23 01:41:09 +00:00
Alex Vandiver
7482a7e61f middleware: Allow HTTP from localhost, not through a reverse proxy.
In servers with `application_server.http_only = true` and
`loadbalancer.ips` set, the DetectProxyMisconfiguration middleware
prevents access over HTTP from IP addresses other than the
loadbalancer.

However, this misses the case of access from localhost over HTTP,
which is safe and expected -- for instance, the `email-mirror-postfix`
script used in the email gateway[^1] will post to `http://localhost/`
by default in such configurations.  With the
DetectProxyMisconfiguration installed, this will result in a 403
response.

Make an exception for requests from `127.0.0.1` and `::1` from
proxy-misconfiguration rejections.

[^1]: https://zulip.readthedocs.io/en/latest/production/email-gateway.html

(cherry picked from commit 5368d1bd4c)
2023-08-17 15:11:43 -04:00
Daniil Fadeev
4c4ec78a3a emails: Fix the issues with image width overflow.
Images in the follow-up day 2 email were overflowing the main email
layout. To prevent that, we created a separate class to handle this.

(cherry picked from commit 673e98e982)
2023-08-15 17:09:35 +00:00
Daniil Fadeev
62b1652b3a emails: Fix the image source for the follow-up day 2 email.
Copied the image from the `/help` directory to the `/emails` directory
 to use it with the `email_images_base_url` variable.

(cherry picked from commit 20dc70d395)
2023-08-15 17:09:35 +00:00
Alex Vandiver
931d989f53 i18n: Update translation data from Transifex. 2023-08-15 01:14:44 +00:00
Alex Vandiver
dbd0457f6e docs: Fix link to "Uprading PostgreSQL" section.
(cherry picked from commit ae3458a294)
2023-08-14 22:58:31 +00:00
David Rosa
1ae5194619 docs: Update .html links pointing to "Upgrade Zulip" or "Modify Zulip".
Follow-up to #24089.

(cherry picked from commit d205850d54)
2023-08-14 22:06:10 +00:00
Alex Vandiver
4331114329 topic: Set a max batch_size on bulk_upate call.
The number of affected objects may be quite high, and they are
selected by `id IN (...)` query, and updated with a giant `CASE`.
This turns out to be quadratic, and can cause large queries to take
hours, in a state where they cannot be terminated, when PostgreSQL >11
tries to JIT the query.

Set a batch_size as a stopgap performance fix before moving to
`.update()` as a real fix.

(cherry picked from commit 570ff08fde)
2023-08-14 22:02:42 +00:00
Lauryn Menard
f907102842 docs: Add multiple-organizations.md to production section of TOC.
Adds the existing article about hosting multiple Zulip organizations
to the production section of the table of contents.
2023-08-11 11:12:20 -07:00
Anders Kaseorg
1338cc32b4 requirements: Upgrade Python requirements.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 29bdaaf5b5)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
3f265b257a profile_request: Support only synchronous responses for now.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 32a8151ce8)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
a715e6aa33 pyproject: Remove mypy exemption for coverage.
coverage added type annotations in 7.2.0.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit ff6cf54716)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
291cf12c45 name_restrictions: Update disposable_email_domains usage.
‘blocklist’ was added in 0.0.35 (with backwards compatibility for the
old name), and type annotations were added in 0.0.91 (with only the
new name).

Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 195efb3802)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
948171a839 ruff: Fix B034 re.split, re.sub should pass keyword arguments.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit d87eea1a67)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
8fdcadb08a ruff: Fix UP032 Use f-string instead of format call.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 50e6cba1af)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
2e9544a9a5 ruff: Fix RUF015 Prefer next(iter(…)) over list(…)[0].
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 0efc662eab)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
a8d6c44db4 openapi: Switch to new openapi_core validation API.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 9bb3d15a79)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
406f9f7852 logging_util: Remove dependence on get_current_request.
Pass the HttpRequest explicitly through the two webhooks that log to
the webhook loggers.

get_current_request is now unused, so remove it (in the same commit
for test coverage reasons).

Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 63be67af80)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
cc21dc8ec7 sentry: Remove dependence on get_current_request.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit f66e2c3112)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
775d9a889c requirements: Upgrade Python requirements.
(cherry picked from commit 784622ee5d)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
89a3a627be codespell: Correct “requestor” to “requester”.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit c09e7d6407)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
a03ea9dc08 django_api: Extract send_event_on_commit helper.
django-stubs 4.2.1 gives transaction.on_commit a more accurate type
annotation, but this exposed that mypy can’t handle the lambda default
parameters that we use to recapture loop variables such as

    for stream_id in public_stream_ids:
        peer_user_ids = …
        event = …

        transaction.on_commit(
            lambda event=event, peer_user_ids=peer_user_ids: send_event(
                realm, event, peer_user_ids
            )
        )

https://github.com/python/mypy/issues/15459

A workaround that mypy accepts is

        transaction.on_commit(
            (
                lambda event, peer_user_ids: lambda: send_event(
                    realm, event, peer_user_ids
                )
            )(event, peer_user_ids)
        )

But that’s kind of ugly and potentially error-prone, so let’s make a
helper function for this very common pattern.

        send_event_on_commit(realm, event, peer_user_ids)

Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 7657cb4a0f)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
252254db69 middleware: Do not consume StreamingHttpResponse.streaming_content.
streaming_content is an iterator. Consuming it within middleware
prevents it from being sent to the browser.

https://docs.djangoproject.com/en/4.2/ref/request-response/#streaminghttpresponse-objects

“The StreamingHttpResponse … has no content attribute. Instead, it has
a streaming_content attribute. This can be used in middleware to wrap
the response iterable, but should not be consumed.”

Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 98310f269b)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
8a6ebeda87 tests: Remove assert_streaming_content helper in favor of getvalue.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 92c83c1df4)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
7bb554d58b openapi: Convert deprecated Spec.create to Spec.from_dict.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 85681546ce)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
1a4214a488 ruff: Fix PLC0208 Use a sequence type when iterating over values.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit b7909db987)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
ba2a359e3f ruff: Fix more of RUF010 Use conversion in f-string.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit b907ad0dcb)
2023-08-10 17:01:52 -05:00
Anders Kaseorg
201cab601a test_helpers: Convert TypedDict from queries_captured to dataclass.
An implicit coercion from an untyped dict to the TypedDict was hiding
a type error: CapturedQuery.sql was really str, not bytes.  We should
always prefer dataclass over TypedDict to prevent such errors.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 92db6eba78)
2023-08-10 17:01:52 -05:00
Alex Vandiver
9628cc9278 retention: Prevent deletion of partially-archived messages.
Previously, this code:
```python3
old_archived_attachments = ArchivedAttachment.objects.annotate(
    has_other_messages=Exists(
        Attachment.objects.filter(id=OuterRef("id"))
        .exclude(messages=None)
        .exclude(scheduled_messages=None)
    )
).filter(messages=None, create_time__lt=delta_weeks_ago, has_other_messages=False)
```

...protected from removal any ArchivedAttachment objects where there
was an Attachment which had _both_ a message _and_ a scheduled
message, instead of _either_ a message _or_ a scheduled message.
Since files are removed from disk when the ArchivedAttachment rows are
deleted, this meant that if an upload was referenced in two messages,
and one was deleted, the file was permanently deleted when the
ArchivedMessage and ArchivedAttachment were cleaned up, despite being
still referenced in live Messages and Attachments.

Switch from `.exclude(messages=None).exclude(scheduled_messages=None)`
to `.exclude(messages=None, scheduled_messages=None)` which "OR"s
those conditions appropriately.

Pull the relevant test into its own file, and expand it significantly
to cover this, and other, corner cases.

(cherry picked from commit b67108c8c6)
2023-08-09 22:41:20 +00:00
Alex Vandiver
97d4f71f0f retention: Do not archive attachments with scheduled messages.
(cherry picked from commit 0f918d9071)
2023-08-09 22:23:17 +00:00
Anders Kaseorg
a8675741a0 error_notify: Remove custom email error reporting handler.
Restore the default django.utils.log.AdminEmailHandler when
ERROR_REPORTING is enabled.  Those with more sophisticated needs can
turn it off and use Sentry or a Sentry-compatible system.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit b285813beb)
2023-07-21 06:55:56 -07:00
Alex Vandiver
290b54a82d upgrade: Check PostgreSQL versions after venv setup.
The claim in the comment from c8ec3dfcf6, that we can and should use
the current deploy's venv, misses one key case -- when upgrading the
operating system, the current deploy's venv is unworkable, since it
was configured for a previous version of Python.  As such, any attempt
to load Django to verify the version of PostgreSQL it is talking to
must happen after the venv is configured.

Move the database version check into
`scripts/lib/check-database-compatibility`, which also moves it after
the new venv is configured.

Because we no longer reliably know, at `apt-get upgrade` time, what
version of PostgreSQL is installed, we hold all versions of the
pgroonga packages.

(cherry picked from commit 1accc6929e)
2023-07-19 18:02:27 -04:00
Akshat
7a448010e2 message_view_header: Fix bad rendering of stream links in description.
This bad rendering was the result of unwanted css applied
in the stream description. In message view header, the stream
link (title) we have defined has css defined but the markdown
rendered stream link in stream description had the same class
resulting in unwanted css applied to it.

Fixes: #25961.

Signed-off-by: Akshat <akshat25iiit@gmail.com>
(cherry picked from commit d302ac4a18)
2023-07-19 18:02:27 -04:00
Alex Vandiver
8f1db3b5c5 export: Skip crossrealm bots, if they are in the exported realm.
This prevents them from being duplicated in the crossrealm users.

(cherry picked from commit 54395612c7)
2023-07-19 18:02:27 -04:00
Alex Vandiver
0e26371c4b import: Merge mirrordummy users _before_ recipients are stripped out.
`remove_denormalized_recipient_column_from_data` removes the
`recipient` data from `zerver_userprofile`, but did not remove it from
`zerver_userprofile_mirrordummy`, which was later appended to the list
of `zerver_userprofile` objects.  This led to failure when inserting,
as the mirrordummy objects still tried to reference their previous
`recipient_id`s.

Move the merging of the two sets earlier, before we call
`remove_denormalized_recipient_column_from_data`.

(cherry picked from commit 207cfe49cf)
2023-07-19 18:02:27 -04:00
Alex Vandiver
fa6f2f1776 export: Include huddles subscription from mirrordummy users.
If there are two huddles, with users A + B + C + D and A + B + C, and
user D is deleted, it is replaced with a mirrordummy user.  If
mirrordummy subscriptions are not included in exports, then the two
huddles have duplicate member sets, and will not be able to be
imported successfully.

Include huddle subscriptions for mirrordummy users in exports.

(cherry picked from commit cfda414277)
2023-07-19 18:02:27 -04:00
Alex Vandiver
56c770987f management: Add a reactivate-stream command.
Fixes #601.

(cherry picked from commit b188e6fa04)
2023-07-19 18:02:27 -04:00
Alex Vandiver
fc472b7b1c puppet: Update dependencies.
(cherry picked from commit 0c44db5325)
2023-07-19 17:34:04 -04:00
Alex Vandiver
eb6d5346fa docs: Mention the PostgreSQL upgrade in the release upgrade section.
Users can, quite understandably, assume that upgrading Zulip upgraded
the underlying PostgreSQL version.  Though it is mentioned at the top
of the page, mentioning it here clarifies that it is an additional
step.

(cherry picked from commit a7b9e67c06)
2023-07-19 17:33:21 -04:00
Alex Vandiver
3349c596d5 release: Release 8.0-prereleases from main.
(cherry picked from commit 23fd05494d)
2023-07-19 17:32:06 -04:00
Alex Vandiver
5a58adf62a release: Link to the changelog in the Github Release.
Release notes in GitHub have significant-newlines, which makes our
canonical `changelog.md` content look not-great -- and the relative
links are also broken, as they have the wrong relative path.

Switch to linking to the canonical release notes on ReadTheDocs, now
that those have proper anchors.

(cherry picked from commit c1008b5b12)
2023-07-19 17:32:06 -04:00
Alex Vandiver
bf68d676c0 changelog: Retitle sections, to allow section anchors.
(cherry picked from commit 9bd340957f)
2023-07-19 17:32:03 -04:00
Alex Vandiver
2dcabec005 docs: Reference PostgreSQL version in "Restoring backups" section.
This extends 8ede54fb1b, in the specific backups section, rather
than just in the overview.

(cherry picked from commit e18d0e3331)
2023-07-19 17:29:46 -04:00
Satyam Bansal
ec46497507 tools: Copy "assets" subdirectory of bots to "static/generated/bots/".
Previously, the "assets" subdirectory of various bots was not
being copied to the "static/generated/bots/" folder, which
resulted in the documentation on some pages not loading at all.

(cherry picked from commit 605e9065a2)
2023-07-19 17:29:14 -04:00
Mateusz Mandera
6c18abddb6 docs: Update SCIM doc to not mention add_scim_client.
add_scim_client was removed in 55342efd33.

(cherry picked from commit 8c530f30e3)
2023-07-19 17:28:22 -04:00
Alex Vandiver
8d9e51a067 docs: Fix a typo in the 7.2 release changelog. 2023-07-05 20:29:34 +00:00
Alex Vandiver
76d1c70f7a version: Update version after 7.2 release. 2023-07-05 20:21:06 +00:00
Alex Vandiver
4174047f64 Release Zulip Server 7.2. 2023-07-05 19:57:28 +00:00
Alex Vandiver
adbee935f7 upload: Provide a default upload file name, rather than 500.
(cherry picked from commit e2847790b6)
2023-07-04 16:31:05 +00:00
Alex Vandiver
ebed224395 i18n: Update translation data from Transifex. 2023-07-03 20:40:46 +00:00
Alex Vandiver
738429cf77 middleware: Detect reverse proxy misconfigurations.
Combine nginx and Django middlware to stop putting misleading warnings
about `CSRF_TRUSTED_ORIGINS` when the issue is untrusted proxies.
This attempts to, in the error logs, diagnose and suggest next steps
to fix common proxy misconfigurations.

See also #24599 and zulip/docker-zulip#403.

(cherry picked from commit 8a77cca341)
2023-07-03 18:52:30 +00:00
Alex Vandiver
2f91471e98 zproject: Prevent having exactly 17/18 middlewares, for Python 3.11 bug.
Having exactly 17 or 18 middlewares, on Python 3.11.0 and above,
causes python to segfault when running tests with coverage; see
https://github.com/python/cpython/issues/106092

Work around this by adding one or two no-op middlewares if we would
hit those unlucky numbers.  We only add them in testing, since
coverage is a requirement to trigger it, and there is no reason to
burden production with additional wrapping.

(cherry picked from commit cf0b803d50)
2023-07-03 18:52:30 +00:00
Alex Vandiver
8f9807176e puppet: Remove loadbalancer configurations when they are unset.
(cherry picked from commit 671b708c4b)
2023-07-03 18:52:30 +00:00
Karl Stolley
217f5731fe stream_settings: Fix scroll at max-height.
Simplebar seems unaware of the `max-height: 1000px` on
`.subscriptions-container`, and therefore does not properly provide
a scrollbar when it's needed.

This commit adds a `max-height` to the stream Simplebar container,
ensuring that otherwise hidden content that Simplebar believes to
be visible can be scrolled to.

Finally, rather than rely on magic numbers or math done in comments,
this commit establishes CSS variables for all relevant modal-element
heights, doing the math inline using CSS calc().

Fixes #26107.

(cherry picked from commit 0c55fb7e89)
2023-07-03 18:51:58 +00:00
Karl Stolley
ca0fd7f797 bot_icon: Adjust bot-icon color for light and dark modes.
(cherry picked from commit a9bc5e94e7)
2023-07-03 18:51:37 +00:00
Alex Vandiver
da4c4f74f2 slack: Handle the special case of permissions denied on team.info call.
This is a follow-up to 4c8915c8e4, for
the case when the `team:read` permission is missing, which causes the
`team.info` call itself to fail.  The error message supplies
information about the provided and missing permissions -- but it also
still sends the `X-OAuth-Scopes` header which we normall read, so we can
use that as normal.

(cherry picked from commit 21aeb4a040)
2023-07-03 18:51:15 +00:00
Alex Vandiver
b2068222e0 pgroonga: Remove 'GRANT USAGE' statement again.
dc2726c814 removed these statements, but c8ec3dfcf6 accidentally
brought one back.  Remove it.

(cherry picked from commit f5540303ba)
2023-07-03 18:50:51 +00:00
Alex Vandiver
2dfc0463bd pgroonga: Run upgrade SQL when pgroonga package is updated.
Updating the pgroonga package is not sufficient to upgrade the
extension in PostgreSQL -- an `ALTER EXTENSION pgroonga UPDATE` must
explicitly be run[^1].  Failure to do so can lead to unexpected behavior,
including crashes of PostgreSQL.

Expand on the existing `pgroonga_setup.sql.applied` file, to track
which version of the PostgreSQL extension has been configured.  If the
file exists but is empty, we run `ALTER EXTENSION pgroonga UPDATE`
regardless -- if it is a no-op, it still succeeds with a `NOTICE`:

```
zulip=# ALTER EXTENSION pgroonga UPDATE;
NOTICE:  version "3.0.8" of extension "pgroonga" is already installed
ALTER EXTENSION
```

The simple `ALTER EXTENSION` is sufficient for the
backwards-compatible case[^1] -- which, for our usage, is every
upgrade since 0.9 -> 1.0.  Since version 1.0 was released in 2015,
before pgroonga support was added to Zulip in 2016, we can assume for
the moment that all pgroonga upgrades are backwards-compatible, and
not bother regenerating indexes.

Fixes: #25989.

[^1]: https://pgroonga.github.io/upgrade/

(cherry picked from commit c8ec3dfcf6)
2023-07-03 18:50:14 +00:00
Alex Vandiver
92c538c862 pgroonga: Remove now-unnecessary 'GRANT USAGE' statement.
This was only necessary for PGroonga 1.x, and the `pgroonga` schema
will most likely be removed at some point inthe future, which will
make this statement error out.

Drop the unnecessary statement.

(cherry picked from commit dc2726c814)
2023-07-03 18:50:14 +00:00
Lauryn Menard
2e03e1b6ee narrow: Mark as read in by_recipient based on case ("dm" or "stream").
In commit #25837, we added in a check for the user's mark as read
policy in the frontend for `by_topic` and `by_recipient` narrowing.
In that change, the assumption was that for both functions, it was
sufficient to check only for whether the user policy was to never
mark as read.

But because the `by_recipient` function may narrow to an interleaved
stream view, it is possible that message will be marked as read
when the user did not expect it to be (e.g. they marked all the
messages in a topic narrow as unread and then used the `S` key
shortcut to navigate back to the stream view) when they have
conversation views only as their mark as read  policy.

Here we move the check for the user's mark as read policy to be in
the two cases for `by_recipient` so that the mark as read behavior
here matches the user's setting.

(cherry picked from commit c5fbd3f085)
2023-07-03 18:49:31 +00:00
Daniil Fadeev
0ac81a1b77 compose_banner: Remove uploads banners when clearing compose box.
Upload banners were not cleared after closing compose box, which meant
that they would remain present in a paused state after compose was reopened.

https://chat.zulip.org/#narrow/stream/9-issues/topic/Incomplete.20Upload.20banner.20remains.20on.20closing.20compose/near/1582602
(cherry picked from commit daab1d4265)
2023-07-03 18:49:10 +00:00
Alex Vandiver
92e840efd1 puppet: Support IPv6 nameservers.
The syntax in `/etc/resolv.conf` does not include any brackets:
```
nameserver 2001:db8::a3
```

However, the format of the nginx `resolver` directive[^1] requires that
IPv6 addresses be enclosed in brackets.

Adjust the `resolver_ip` puppet function to surround any IPv6
addresses extracted from `/etc/resolv.conf` with square brackets, and
any addresses from `application_server.resolver` to gain brackets if
necessary.

Fixes: #26013.

[^1]: http://nginx.org/en/docs/http/ngx_http_core_module.html#resolver

(cherry picked from commit 7ef05316d5)
2023-07-03 18:48:47 +00:00
Alex Vandiver
f1a8c402d1 convert_slack_data: Document "--token" more correctly.
(cherry picked from commit 38d1b3314a)
2023-07-03 18:48:08 +00:00
Alex Vandiver
8878fee6d6 slack: Provide more information when a Slack token fails to validate.
(cherry picked from commit 4c8915c8e4)
2023-07-03 18:48:08 +00:00
Alex Vandiver
783f3fac3b test_slack_importer: Switch to xoxb tokens, which is what we accept.
(cherry picked from commit 1b2ba4e09d)
2023-07-03 18:48:08 +00:00
Sahil Batra
5f397e5fa8 stream_settings: Fix height of select elements.
Previously, we used to have top and bottom paddings of 4px to
the select elements but it was removed in a208da9c4d to make
sure that text for the selected option is aligned properly.

All other select elements have height set to 30px, but the
select elements in stream settings page had height set to
"fit-content" and so they looked ugly after removing the
padding.

This commit sets the height of select elements in stream
settings to 30px.

(cherry picked from commit b119ff68c3)
2023-07-03 18:47:31 +00:00
Daniil Fadeev
8ff2684d61 compose: Fix keyboard indicator appearance in send shortcut.
(cherry picked from commit b13a85cdbf)
2023-07-03 18:46:41 +00:00
Daniil Fadeev
016f53711d compose: Fix keyboard indicator vertical alignment in send shortcut.
(cherry picked from commit 83b4fef060)
2023-07-03 18:46:41 +00:00
Alex Vandiver
e921c7dafe docs: Clarify that trust of X-Fowarded-Proto is also necessary.
Previously, `X-Forwarded-Proto` did not need to be set, and failure to
set `loadbalancer.ips` would merely result in bad IP-address
rate-limiting and incorrect access logs; after 0935d388f0, however,
failure to do either of those, if Zulip is deployed with `http_only`,
will lead to infinite redirect loops after login.  These are
accompanied by a misleading error, from Tornado, of:

    Forbidden (Origin checking failed - https://zulip.example.com does not match any trusted origins.): /json/events

This is most common with Docker deployments, where deployments use
another docker container, such as nginx or Traefik, to do SSL
termination.  See zulip/docker-zulip#403.

Update the documentation to reinforce that `loadbalancer.ips` also
controls trust of `X-Forwarded-Proto`, and that failure to set it will
cause the application to not function correctly.

(cherry picked from commit d46279c41e)
2023-07-03 18:45:54 +00:00
Alex Vandiver
9b950f9c6a send_email: Delete ScheduledEmail objects with no recipients.
9d97af6ebb addressed the one major source of inconsistent data which
would be solved by simply re-attempting the ScheduledEmail row.  Every
other instance that we have seen since then has been a corrupt or
modified database in some way, which does not self-resolve.  This
results in an endless stream of emails to the administrator, and no
forward progress.

Drop this to a warning, and make it remove the offending row.  This
ensures we make forward progress.

(cherry picked from commit 77c146b8b0)
2023-07-03 18:44:33 +00:00
Alex Vandiver
aab515feb9 version: Update version after 7.1 release. 2023-06-13 18:15:14 +00:00
Alex Vandiver
b178bb7c59 Release Zulip Server 7.1. 2023-06-13 17:59:16 +00:00
Alex Vandiver
8a783c448f i18n: Update translation data from Transifex. 2023-06-13 17:28:04 +00:00
Alya Abbott
f4f8d091a9 github: Configure templates for filing issues.
This PR creates templates for filing issues. The templates are
intentionally quite light-weight. Note that I'm specifically not using
forms for creating issues, as the UI for filling out such a form does
not include GitHub's helpful formatting buttons and preview mode.

Follow-up to #25998, pushed as a separate PR so that the original one can pass CI.

This PR creates templates for filing issues. The templates are
intentionally quite light-weight. Note that I'm specifically not using
forms for creating issues, as the UI for filling out such a form does
not include GitHub's helpful formatting buttons and preview mode.

A major goal is to guide users towards starting a CZO conversation
when that's more appropriate than filing a GitHub issue.

Note that the config makes it possible to create a blank issue, which
should be handy for:

* Issues filed by maintainers
* Issues for tracking follow-ups from merged PRs
* Probably some other situations

Because the blank issue option is easy to miss, it should probably be
documented somewhere, but I'm not sure where. We can perhaps start
with a note on CZO.

Relevant CZO threads:

https://chat.zulip.org/#narrow/stream/137-feedback/topic/issues.20link.20in.20description/near/1561110)
https://chat.zulip.org/#narrow/stream/2-general/topic/bug.20report.20management/near/1589141

Also provide external documentation links for situations where
filing an issue is not the best approach.

(cherry picked from commit 0adcc2a1df)
2023-06-13 11:48:50 -04:00
Alya Abbott
67157ec2b8 docs: Link to new guide on suggesting features and improvements.
(cherry picked from commit 03659004a9)
2023-06-13 11:48:50 -04:00
Alex Vandiver
4bf4c8a040 upgrade-postgresql: Only upgrade to a supported version.
(cherry picked from commit 875502b2e1)
2023-06-13 11:48:50 -04:00
Alex Vandiver
08a844153c docs: Document supported versions of PostgreSQL.
Fixes: #25853.
(cherry picked from commit f4b20337a7)
2023-06-13 11:48:50 -04:00
Alya Abbott
69e04c20f9 docs: Add a guide on suggesting features and improvements.
Similar to the guide on reporting bugs.

(cherry picked from commit 9ee5a5a70e)
2023-06-13 11:48:50 -04:00
Alya Abbott
ec8d341cae docs: Link to new "Reporting bugs" guide.
(cherry picked from commit 9258acce14)
2023-06-13 11:48:50 -04:00
Alya Abbott
3ee59df091 docs: Improve instructions for reporting bugs.
- Create a dedicated "Reporting bugs" page to learly document
where and how bugs should be reported.
- Drop "Reporting issues" section from the Contributing guide.
- Delete "Bug report guidelines" page.

(cherry picked from commit 052a109ba4)
2023-06-13 11:48:50 -04:00
Alex Vandiver
dd940d2eac puppet: Read resolver from /etc/resolv.conf.
04cf68b45e make nginx responsible for downloading (and caching)
files from S3.  As noted in that commit, nginx implements its own
non-blocking DNS resolver, since the base syscall is blocking, so
requires an explicit nameserver configuration.  That commit used
127.0.0.53, which is provided by systemd-resolved, as the resolver.

However, that service may not always be enabled and running, and may
in fact not even be installed (e.g. on Docker).  Switch to parsing
`/etc/resolv.conf` and using the first-provided nameserver.  In many
deployments, this will still be `127.0.0.53`, but for others it will
provide a working DNS server which is external to the host.

In the event that a server is misconfigured and has no resolvers in
`/etc/resolv.conf`, it will error out:
```console
Error: Evaluation Error: Error while evaluating a Function Call, No nameservers found in /etc/resolv.conf!  Configure one by setting application_server.nameserver in /etc/zulip/zulip.conf (file: /home/zulip/deployments/current/puppet/zulip/manifests/app_frontend_base.pp, line: 76, column: 70) on node example.zulipdev.org
```

(cherry picked from commit bd217ad31b)
2023-06-12 21:12:50 +00:00
Tim Abbott
e3f0c28528 docs: Improve troubleshooting overview intro.
(cherry picked from commit 6ca5130cd8)
2023-06-12 20:09:13 +00:00
Alya Abbott
b44ee89245 docs: Clarify instructions for getting help with self-hosting.
(cherry picked from commit 582e88544c)
2023-06-12 20:09:13 +00:00
Alex Vandiver
ee2654c4ee uploads: Allow access to the /download/ variant anonymously.
This was mistakenly left off of b799ec32b0.

(cherry picked from commit fbb831ff3b)
2023-06-12 20:05:46 +00:00
Alex Vandiver
c12f8de80b test_helpers: Switch add/remove_ratelimit to a contextmanager.
Failing to remove all of the rules which were added causes action at a
distance with other tests.  The two methods were also only used by
test code, making their existence in zerver.lib.rate_limiter clearly
misplaced.

This fixes one instance of a mis-balanced add/remove, which caused
tests to start failing if run non-parallel and one more anonymous
request was added within a rate-limit-enabled block.

(cherry picked from commit 0dbe111ab3)
2023-06-12 20:05:46 +00:00
Sahil Batra
94437ab5be user_groups: Prevent cycles when adding subgroups for a user group.
The user group depedency graph should always be a DAG.
This commit adds code to make sure we keep the graph DAG
while adding subgroups to a user group.

Fixes #25913.
2023-06-12 16:04:18 -04:00
Zixuan James Li
a9a30ad5b4 user_groups: Make system groups creation atomic.
We want to make sure that the system groups, once created, will always
have the GroupGroupMemberships fully set up.

Signed-off-by: Zixuan James Li <p359101898@gmail.com>
2023-06-12 16:04:18 -04:00
Alex Vandiver
e9f0ef4c15 docs: Clarify access to port 25 is needed for local email delivery. 2023-06-12 16:04:18 -04:00
Alex Vandiver
2e16e27265 ui_init: Fix typo in error data. 2023-06-12 16:04:18 -04:00
Karl Stolley
f5e2a2a38f popovers: Reorganize Tippy onShow logic for stream popover.
These changes appear to correct the keyboard-navigation repro
from #25907, and it makes it possible for users without the
permission to create streams to exit the streams modal by
hitting Esc.

This reorganizes logic within the Tippy `onShow` method to
ensure that nothing is set or called for those users without
stream-creation privileges.

These changes probably require broader testing to determine
whether the fix addresses only that specific reproducer, or
the broader problems #25907 addresses with malfunctioning
j, k, Esc, and Return keys (when Ctrl + Return to send is
enabled).

Fixes a part of #25907.
2023-06-12 16:04:18 -04:00
Alex Vandiver
5a177bff3a prod_settings_template: Document REALM_HOSTS configuration.
This was in docs/production/multiple-organizations.md, but not the
settings template.
2023-06-12 16:04:18 -04:00
Alex Vandiver
fabb5ffe94 upgrade-zulip: Verify postgresql.version against where data is stored.
This prevents installing a PostgreSQL server which matches
/etc/zulip/zulip.conf but which has no data and is not used by Django.
2023-06-12 16:04:18 -04:00
Alex Vandiver
5b4a673bbd upgrade-zulip: Set postgresql.version from running version, not a guess. 2023-06-12 16:04:18 -04:00
Alex Vandiver
afeb73e12a upgrade-zulip: Simplify PostgreSQL version check.
This is much simpler now that we do not support PostgreSQL 9.x.
2023-06-12 16:04:18 -04:00
Alex Vandiver
d5a39a6564 upgrade-postgresql: Prevent upgrades if /etc/zulip/zulip.conf is wrong.
If the `postgresql.version` in `/etc/zulip/zulip.conf` is out of date
or wrong, upgrading to the actual current version would drop your
production database without prompting.  While we do document taking a
Zulip backup (which includes a database backup) before running
`upgrade-postgresql`[^1], not everyone does so, with possibly
catastrophic consequences.

Do a true end-to-end check of the version in `/etc/zulip/zulip.conf`
by asking Django to query the database for its version, checking that
against the configured value, and aborting if there is any
disagreement.

[^1]: https://zulip.readthedocs.io/en/latest/production/upgrade.html#upgrading-postgresql
2023-06-12 16:04:18 -04:00
Alex Vandiver
a4b5ee41ea upgrade-postgresql: Prevent PostgreSQL downgrades. 2023-06-12 16:04:18 -04:00
Alex Vandiver
884a8d5628 upgrade-postgresql: Check for undefined variables. 2023-06-12 16:04:18 -04:00
Mateusz Mandera
8c9e521f57 migrations: Handle duplicate fk constraint in 0443.
It turns out that for some some deployments, there exists a second,
duplicate, foreign key constraint for user_profile_id. The logic below
would try to rename both to the same name, which would fail on the
second:

```
psycopg2.errors.DuplicateObject: constraint "zerver_userpresenceo_user_profile_id_d75366d6_fk_zerver_us" for relation "zerver_userpresence" already exists
```

Eliminate the duplicate constraint, rather than attempting to rename
it.  Also add a block, in case of future reuse of this pattern, which
caveats that this approach will not work in the presence of
explicitly-named indexes.  UserPresence happens to not have any, so
this technique is safe in this instance.

Co-authored-by: Alex Vandiver <alexmv@zulip.com>
2023-06-12 16:04:18 -04:00
Brijmohan Siyag
2f04875ad3 send_later: Wildcard mention throwing error on send later.
It was throwing error while schudiling a message having wildcard mention,
because the function `open_send_later_menu` was using param instance to track down
interval, but the parametere instance was not passed from when it was
called from warning banner action. This commit removes the instance
param as it is of no use, and uses a variable to track interval.
2023-06-12 16:04:18 -04:00
Anders Kaseorg
9dcf1944ad install: Check CPU and OS architecture.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-06-12 16:04:18 -04:00
Anders Kaseorg
6b895d1622 install: Add system_requirements_failure helper.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-06-12 16:04:18 -04:00
Mateusz Mandera
1a65e8a538 migrations: Fix bug in migration 0439.
This code clearly meant to return host and returning realm.host is a
mistake. realm.host is not accessible in a migration due to being a
@property-decorated method. The code constructs the host var value just
above this line.

(cherry picked from commit a55901aa67)
2023-06-05 16:33:22 +00:00
Tim Abbott
3240a513f1 version: Update version after 7.0 release. 2023-05-31 09:20:26 -07:00
3434 changed files with 189766 additions and 316539 deletions

View File

@@ -25,4 +25,3 @@ forin
uper
slac
couldn
ges

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -136,8 +136,7 @@ Here are some guidelines for you how can help:
Ive 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 someones behavior is poor or disrespectful.
and positive tone even when someones 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.

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ Come find us on the [development community chat](https://zulip.com/development-c
[![GitHub Actions build status](https://github.com/zulip/zulip/actions/workflows/zulip-ci.yml/badge.svg)](https://github.com/zulip/zulip/actions/workflows/zulip-ci.yml?query=branch%3Amain)
[![coverage status](https://img.shields.io/codecov/c/github/zulip/zulip/main.svg)](https://codecov.io/gh/zulip/zulip)
[![Mypy coverage](https://img.shields.io/badge/mypy-100%25-green.svg)][mypy-coverage]
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json)](https://github.com/charliermarsh/ruff)
[![code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![GitHub release](https://img.shields.io/github/release/zulip/zulip.svg)](https://github.com/zulip/zulip/releases/latest)

View File

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

View File

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

View File

@@ -30,5 +30,4 @@ def time_range(
while current >= start:
times.append(current)
current -= step
times.reverse()
return times
return list(reversed(times))

View File

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

View File

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

View File

@@ -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.")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": [],

View File

@@ -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": [],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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