mirror of
https://github.com/zulip/zulip.git
synced 2025-10-24 16:43:57 +00:00
Compare commits
229 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c805268f2d | ||
|
cb8dc451a8 | ||
|
e630281275 | ||
|
007708a626 | ||
|
d16b40a156 | ||
|
e5a3d20a5c | ||
|
ad6867b6cc | ||
|
175ec1f365 | ||
|
1a8429e338 | ||
|
6608c87772 | ||
|
5606489d96 | ||
|
bc7ea80452 | ||
|
2b6058d5f7 | ||
|
176a8bd3df | ||
|
38421b77ea | ||
|
2fc5040b0b | ||
|
a0c1bf1e02 | ||
|
3095a7b439 | ||
|
9002cf750f | ||
|
171f902fe1 | ||
|
1205d7603c | ||
|
9c1fcfb69f | ||
|
ea27b848fc | ||
|
836ab28a75 | ||
|
8cff227ecf | ||
|
fdea941046 | ||
|
496f793dae | ||
|
ff4860ddbc | ||
|
456a644fad | ||
|
428081bf14 | ||
|
77da214e3c | ||
|
358a9a5fbe | ||
|
e29bcff2fb | ||
|
ba5f68c155 | ||
|
cfa6e41691 | ||
|
b0f6e3058e | ||
|
1462fe7bb2 | ||
|
cd877aba5a | ||
|
612aaae283 | ||
|
6b9365f616 | ||
|
a5ee0e913e | ||
|
f388030e85 | ||
|
c657b6cc72 | ||
|
e82a693576 | ||
|
84a1024b09 | ||
|
c1be5e6766 | ||
|
3c4d473708 | ||
|
e37cd63001 | ||
|
2db8371074 | ||
|
9d9ab65dcc | ||
|
d4f95bc1f9 | ||
|
e533b7c017 | ||
|
fccd5f5e82 | ||
|
c0cd582f0c | ||
|
b0589bf286 | ||
|
a653a73af2 | ||
|
7074abd86e | ||
|
53f885fa15 | ||
|
43c6e3d47f | ||
|
7911ca0cee | ||
|
bf1c5e08ec | ||
|
68c9866a6e | ||
|
5c1451dd21 | ||
|
2e4fca6daa | ||
|
c1be1e0116 | ||
|
de42b4d1ad | ||
|
819b16b11e | ||
|
8276dcdb30 | ||
|
087a89b2f4 | ||
|
d41dc8eeae | ||
|
91da77d4e8 | ||
|
dbfe4ddee4 | ||
|
fb083bafec | ||
|
15fbcafed8 | ||
|
a04dd1bb92 | ||
|
859cc29657 | ||
|
7ee999917f | ||
|
d6fadeec77 | ||
|
9bb9c20c88 | ||
|
69ac1c0724 | ||
|
e97d532811 | ||
|
fea421b54d | ||
|
4cb838168f | ||
|
6ea67a7df2 | ||
|
03ebeb10ab | ||
|
c65cc48215 | ||
|
25d1491999 | ||
|
bc3753d859 | ||
|
33f4cd1ad4 | ||
|
4bc70f7c04 | ||
|
b5ab90aaa4 | ||
|
9423f213a7 | ||
|
6abbbc190c | ||
|
b61da7d944 | ||
|
73669ff7f0 | ||
|
ed5fc4cc19 | ||
|
85b2e6a1e9 | ||
|
f4279a2a7f | ||
|
7fd018d82a | ||
|
4a35e00d1c | ||
|
e44108edb2 | ||
|
d7293735e1 | ||
|
da72e9447e | ||
|
c357eb8225 | ||
|
03feb5a546 | ||
|
bcd88fdb68 | ||
|
e6291a540c | ||
|
f539147446 | ||
|
45f30a19e1 | ||
|
f131269395 | ||
|
d8501197ee | ||
|
3a89ca6b46 | ||
|
a906bd4b33 | ||
|
2ef119b62b | ||
|
7f8bc37cf5 | ||
|
9ef4649406 | ||
|
9fbf4527a8 | ||
|
ab81867721 | ||
|
b7e38f4dd6 | ||
|
4f86630faa | ||
|
eeecb995ca | ||
|
1e6a413895 | ||
|
21691024d2 | ||
|
d3ff0cb95f | ||
|
8dcc2bf592 | ||
|
1c6fba6c8f | ||
|
5918266544 | ||
|
e916abf31e | ||
|
25c8d2abd0 | ||
|
1f442f0bd1 | ||
|
7183621e87 | ||
|
748ce899d2 | ||
|
d7ee758ca2 | ||
|
693e06bf63 | ||
|
9077cd6467 | ||
|
c9182efcea | ||
|
a59b9b3f66 | ||
|
9c7a00faaf | ||
|
0232a4c92e | ||
|
0e3eb0081b | ||
|
71348739be | ||
|
c3401557b7 | ||
|
851953a729 | ||
|
d4532683bb | ||
|
3bbd490d7b | ||
|
b22f514bb7 | ||
|
13e59d590e | ||
|
744b7c7382 | ||
|
cce3c7ebb1 | ||
|
9b33e3bb14 | ||
|
d0cdbab1c0 | ||
|
28fee7aab8 | ||
|
38de0ce7af | ||
|
ffd7e4a426 | ||
|
29a05bb16f | ||
|
39f1e1951b | ||
|
ff876d2df4 | ||
|
3c43603607 | ||
|
3c1fae1707 | ||
|
e57b6719fa | ||
|
9da4eeaa94 | ||
|
13303fd916 | ||
|
747e73470e | ||
|
ceb32a7285 | ||
|
8b9516fb0b | ||
|
3a0de29f5d | ||
|
69e165f0fd | ||
|
e23673141a | ||
|
f1df8f3efa | ||
|
cd97468587 | ||
|
5458a2ca2f | ||
|
19dd4f67ce | ||
|
7905491fa2 | ||
|
36339b6998 | ||
|
af3eefb951 | ||
|
39e43838c3 | ||
|
5f0844d7fb | ||
|
a7bc77aaa0 | ||
|
2e6eeabac6 | ||
|
3f37ee7bc7 | ||
|
b340286e53 | ||
|
558ed44d4b | ||
|
7efac715a8 | ||
|
735a604d8b | ||
|
10e0405220 | ||
|
6396dc5cad | ||
|
b5c5853027 | ||
|
dfa6f67ea8 | ||
|
c9ffd17d2d | ||
|
f1461c5334 | ||
|
03ecbd6654 | ||
|
338fd40ab0 | ||
|
44fdbe5f04 | ||
|
9080684585 | ||
|
2efef3a0e6 | ||
|
1345944688 | ||
|
2dd46fe522 | ||
|
698c827693 | ||
|
319e6de495 | ||
|
fa0da5e9ab | ||
|
b047bf4d44 | ||
|
260c56ab0c | ||
|
b7919e1957 | ||
|
26c983d717 | ||
|
c1d321012a | ||
|
ac9a97e1a7 | ||
|
e6f3c15a92 | ||
|
06172ea126 | ||
|
003ea23eb3 | ||
|
e9f1d5a4ca | ||
|
5aeda0417d | ||
|
7f85d045db | ||
|
1e896d9878 | ||
|
5d947cb501 | ||
|
b99656d3f2 | ||
|
611085abc2 | ||
|
c1a00e7308 | ||
|
c2fc886a8a | ||
|
c2484b01de | ||
|
a42d31bcb2 | ||
|
e3d4677ac5 | ||
|
f12d72d711 | ||
|
30e52c90e5 | ||
|
b78307d559 | ||
|
f902a39ac9 | ||
|
6ef28773a1 | ||
|
1a82ce38af | ||
|
f972e4f832 | ||
|
367f1e634a |
@@ -29,4 +29,3 @@ ges
|
||||
assertIn
|
||||
thirdparty
|
||||
asend
|
||||
COO
|
||||
|
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -20,6 +20,7 @@ corporate/tests/stripe_fixtures/*.json -diff
|
||||
*.eot binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.svg binary
|
||||
*.ttf binary
|
||||
*.png binary
|
||||
*.otf binary
|
||||
@@ -29,6 +30,3 @@ corporate/tests/stripe_fixtures/*.json -diff
|
||||
*.bmp binary
|
||||
*.mp3 binary
|
||||
*.pdf binary
|
||||
|
||||
# Treat SVG files as code for diffing purposes.
|
||||
*.svg diff
|
||||
|
6
.github/ISSUE_TEMPLATE/2_bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/2_bug_report.md
vendored
@@ -11,8 +11,8 @@ labels: ["bug"]
|
||||
**Zulip Server and web app version:**
|
||||
|
||||
- [ ] Zulip Cloud (`*.zulipchat.com`)
|
||||
- [ ] Zulip Server 11.x
|
||||
- [ ] Zulip Server 10.x
|
||||
- [ ] Zulip Server 9.x
|
||||
- [ ] Zulip Server 8.x or older
|
||||
- [ ] Zulip Server 8.x
|
||||
- [ ] Zulip Server 7.x
|
||||
- [ ] Zulip Server 6.x or older
|
||||
- [ ] Other or not sure
|
||||
|
@@ -1,46 +0,0 @@
|
||||
name: Check feature level updated
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "api_docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-feature-level-updated:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Add required permissions
|
||||
run: chmod +x ./tools/check-feature-level-updated
|
||||
|
||||
- name: Run tools/check-feature-level-updated
|
||||
id: run_check
|
||||
run: ./tools/check-feature-level-updated >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Report status to CZO
|
||||
if: ${{ steps.run_check.outputs.fail == 'true' && github.repository == 'zulip/zulip'}}
|
||||
uses: zulip/github-actions-zulip/send-message@v1
|
||||
with:
|
||||
api-key: ${{ secrets.ZULIP_BOT_KEY }}
|
||||
email: "github-actions-bot@chat.zulip.org"
|
||||
organization-url: "https://chat.zulip.org"
|
||||
to: "automated testing"
|
||||
topic: ${{ steps.run_check.outputs.topic }}
|
||||
type: "stream"
|
||||
content: ${{ steps.run_check.outputs.content }}
|
||||
|
||||
- name: Fail job if feature level not updated in API docs
|
||||
if: ${{ steps.run_check.outputs.fail == 'true' }}
|
||||
run: exit 1
|
5
.github/workflows/production-suite.yml
vendored
5
.github/workflows/production-suite.yml
vendored
@@ -154,11 +154,6 @@ jobs:
|
||||
os: bookworm
|
||||
extra-args: --test-custom-db
|
||||
|
||||
- docker_image: zulip/ci:trixie
|
||||
name: Debian 13 production install
|
||||
os: trixie
|
||||
extra-args: ""
|
||||
|
||||
name: ${{ matrix.name }}
|
||||
container:
|
||||
image: ${{ matrix.docker_image }}
|
||||
|
8
.github/workflows/zulip-ci.yml
vendored
8
.github/workflows/zulip-ci.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Base images are built using `tools/ci/Dockerfile`.
|
||||
# Base images are built using `tools/ci/Dockerfile.prod.template`.
|
||||
# The comments at the top explain how to build and upload these images.
|
||||
# Ubuntu 22.04 ships with Python 3.10.12.
|
||||
- docker_image: zulip/ci:jammy
|
||||
@@ -48,12 +48,6 @@ jobs:
|
||||
os: noble
|
||||
include_documentation_tests: false
|
||||
include_frontend_tests: false
|
||||
# Debian 13 ships with Python 3.13.5.
|
||||
- docker_image: zulip/ci:trixie
|
||||
name: Debian 13 (Python 3.13, backend)
|
||||
os: trixie
|
||||
include_documentation_tests: false
|
||||
include_frontend_tests: false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
name: ${{ matrix.name }}
|
||||
|
1
.mailmap
1
.mailmap
@@ -31,7 +31,6 @@ Aman Agrawal <amanagr@zulip.com>
|
||||
Aman Agrawal <amanagr@zulip.com> <f2016561@pilani.bits-pilani.ac.in>
|
||||
Aman Vishwakarma <vishwakarmarambhawan572@gmail.com>
|
||||
Aman Vishwakarma <vishwakarmarambhawan572@gmail.com> <185982038+whilstsomebody@users.noreply.github.com>
|
||||
Aman Vishwakarma <vishwakarmarambhawan572@gmail.com> <whilstsomebody@gmail.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>
|
||||
|
40
.tx/config
Normal file
40
.tx/config
Normal file
@@ -0,0 +1,40 @@
|
||||
# Migrated from transifex-client format with `tx migrate`
|
||||
#
|
||||
# See https://developers.transifex.com/docs/using-the-client which hints at
|
||||
# this format, but in general, the headings are in the format of:
|
||||
#
|
||||
# [o:<org>:p:<project>:r:<resource>]
|
||||
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = zh-Hans: zh_Hans
|
||||
|
||||
[o:zulip:p:zulip:r:djangopo]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/django.po
|
||||
source_file = locale/en/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[o:zulip:p:zulip:r:mobile]
|
||||
file_filter = locale/<lang>/mobile.json
|
||||
source_file = locale/en/mobile.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
||||
[o:zulip:p:zulip:r:translationsjson]
|
||||
file_filter = locale/<lang>/translations.json
|
||||
source_file = locale/en/translations.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
||||
[o:zulip:p:zulip-test:r:djangopo]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/django.po
|
||||
source_file = locale/en/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[o:zulip:p:zulip-test:r:translationsjson]
|
||||
file_filter = locale/<lang>/translations.json
|
||||
source_file = locale/en/translations.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
@@ -8,8 +8,8 @@ Zulip every day. Zulip is the only [modern team chat app][features] that is
|
||||
designed for both live and asynchronous conversations.
|
||||
|
||||
Zulip is built by a distributed community of developers from all around the
|
||||
world, with 97+ people who have each contributed 100+ commits. With
|
||||
over 1,500 contributors merging over 500 commits a month, Zulip is the
|
||||
world, with 74+ people who have each contributed 100+ commits. With
|
||||
over 1000 contributors merging over 500 commits a month, Zulip is the
|
||||
largest and fastest growing open source team chat project.
|
||||
|
||||
Come find us on the [development community chat](https://zulip.com/development-community/)!
|
||||
|
@@ -631,9 +631,9 @@ def count_message_type_by_user_query(realm: Realm | None) -> QueryFn:
|
||||
(
|
||||
SELECT zerver_userprofile.realm_id, zerver_userprofile.id, count(*),
|
||||
CASE WHEN
|
||||
zerver_recipient.type = 1 OR (zerver_recipient.type = 3 AND zerver_huddle.group_size <= 2) THEN 'private_message'
|
||||
zerver_recipient.type = 1 THEN 'private_message'
|
||||
WHEN
|
||||
zerver_recipient.type = 3 AND zerver_huddle.group_size > 2 THEN 'huddle_message'
|
||||
zerver_recipient.type = 3 THEN 'huddle_message'
|
||||
WHEN
|
||||
zerver_stream.invite_only = TRUE THEN 'private_stream'
|
||||
ELSE 'public_stream'
|
||||
@@ -650,15 +650,12 @@ def count_message_type_by_user_query(realm: Realm | None) -> QueryFn:
|
||||
JOIN zerver_recipient
|
||||
ON
|
||||
zerver_message.recipient_id = zerver_recipient.id
|
||||
LEFT JOIN zerver_huddle
|
||||
ON
|
||||
zerver_recipient.type_id = zerver_huddle.id
|
||||
LEFT JOIN zerver_stream
|
||||
ON
|
||||
zerver_recipient.type_id = zerver_stream.id
|
||||
GROUP BY
|
||||
zerver_userprofile.realm_id, zerver_userprofile.id,
|
||||
zerver_recipient.type, zerver_stream.invite_only, zerver_huddle.group_size
|
||||
zerver_recipient.type, zerver_stream.invite_only
|
||||
) AS subquery
|
||||
GROUP BY realm_id, id, message_type
|
||||
"""
|
||||
|
@@ -22,11 +22,10 @@ from zerver.lib.create_user import create_user
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
from zerver.lib.storage import static_path
|
||||
from zerver.lib.stream_color import STREAM_ASSIGNMENT_COLORS
|
||||
from zerver.lib.stream_subscription import create_stream_subscription
|
||||
from zerver.lib.streams import get_default_values_for_stream_permission_group_settings
|
||||
from zerver.lib.timestamp import floor_to_day
|
||||
from zerver.lib.upload import upload_message_attachment_from_request
|
||||
from zerver.models import Client, Realm, RealmAuditLog, Recipient, Stream, UserProfile
|
||||
from zerver.models import Client, Realm, RealmAuditLog, Recipient, Stream, Subscription, UserProfile
|
||||
from zerver.models.groups import NamedUserGroup, SystemGroups, UserGroupMembership
|
||||
from zerver.models.realm_audit_logs import AuditLogEventType
|
||||
|
||||
@@ -126,10 +125,10 @@ class Command(ZulipBaseCommand):
|
||||
stream.save(update_fields=["recipient"])
|
||||
|
||||
# Subscribe shylock to the stream to avoid invariant failures.
|
||||
create_stream_subscription(
|
||||
user_profile=shylock,
|
||||
Subscription.objects.create(
|
||||
recipient=recipient,
|
||||
stream=stream,
|
||||
user_profile=shylock,
|
||||
is_user_active=shylock.is_active,
|
||||
color=STREAM_ASSIGNMENT_COLORS[0],
|
||||
)
|
||||
RealmAuditLog.objects.create(
|
||||
|
@@ -10,7 +10,7 @@ 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 zerver.lib.management import ZulipBaseCommand, abort_cron_during_deploy, abort_unless_locked
|
||||
from zerver.lib.management import ZulipBaseCommand, abort_unless_locked
|
||||
from zerver.lib.remote_server import send_server_data_to_push_bouncer, should_send_analytics_data
|
||||
from zerver.lib.timestamp import floor_to_hour
|
||||
from zerver.models import Realm
|
||||
@@ -38,7 +38,6 @@ class Command(ZulipBaseCommand):
|
||||
)
|
||||
|
||||
@override
|
||||
@abort_cron_during_deploy
|
||||
@abort_unless_locked
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
self.run_update_analytics_counts(options)
|
||||
|
@@ -50,7 +50,11 @@ 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
|
||||
from zerver.lib.push_notifications import (
|
||||
get_message_payload_apns,
|
||||
get_message_payload_gcm,
|
||||
hex_to_b64,
|
||||
)
|
||||
from zerver.lib.streams import get_default_values_for_stream_permission_group_settings
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.test_helpers import activate_push_notification_service
|
||||
@@ -72,9 +76,8 @@ from zerver.models import (
|
||||
from zerver.models.clients import get_client
|
||||
from zerver.models.messages import Attachment
|
||||
from zerver.models.realm_audit_logs import AuditLogEventType
|
||||
from zerver.models.recipients import get_or_create_direct_message_group
|
||||
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||
from zerver.models.users import get_user_by_delivery_email, is_cross_realm_bot_email
|
||||
from zerver.models.users import get_user, is_cross_realm_bot_email
|
||||
from zilencer.models import (
|
||||
RemoteInstallationCount,
|
||||
RemotePushDeviceToken,
|
||||
@@ -504,8 +507,8 @@ class TestCountStats(AnalyticsTestCase):
|
||||
name=f"stream {minutes_ago}", realm=self.second_realm, date_created=creation_time
|
||||
)[1]
|
||||
self.create_message(user, recipient, date_sent=creation_time)
|
||||
self.hourly_user = get_user_by_delivery_email("user-1@second.analytics", self.second_realm)
|
||||
self.daily_user = get_user_by_delivery_email("user-61@second.analytics", self.second_realm)
|
||||
self.hourly_user = get_user("user-1@second.analytics", self.second_realm)
|
||||
self.daily_user = get_user("user-61@second.analytics", self.second_realm)
|
||||
|
||||
# This realm should not show up in the *Count tables for any of the
|
||||
# messages_* CountStats
|
||||
@@ -718,56 +721,6 @@ class TestCountStats(AnalyticsTestCase):
|
||||
)
|
||||
self.assertTableState(StreamCount, [], [])
|
||||
|
||||
def test_1_to_1_and_self_messages_sent_by_message_type_using_direct_group_message(self) -> None:
|
||||
stat = COUNT_STATS["messages_sent:message_type:day"]
|
||||
self.current_property = stat.property
|
||||
|
||||
user1 = self.create_user(is_bot=True)
|
||||
user2 = self.create_user()
|
||||
user3 = self.create_user()
|
||||
|
||||
user1_and_user2_dm_group = get_or_create_direct_message_group([user1.id, user2.id])
|
||||
user2_and_user3_dm_group = get_or_create_direct_message_group([user2.id, user3.id])
|
||||
user2_dm_group = get_or_create_direct_message_group([user2.id])
|
||||
|
||||
assert user1_and_user2_dm_group.recipient is not None
|
||||
assert user2_and_user3_dm_group.recipient is not None
|
||||
assert user2_dm_group.recipient is not None
|
||||
|
||||
self.create_message(user1, user1_and_user2_dm_group.recipient)
|
||||
self.create_message(user2, user2_and_user3_dm_group.recipient)
|
||||
self.create_message(user2, user2_dm_group.recipient)
|
||||
|
||||
do_fill_count_stat_at_hour(stat, self.TIME_ZERO)
|
||||
|
||||
self.assertTableState(
|
||||
UserCount,
|
||||
["value", "subgroup", "user"],
|
||||
[
|
||||
[1, "private_message", user1],
|
||||
[2, "private_message", user2],
|
||||
[1, "public_stream", self.hourly_user],
|
||||
[1, "public_stream", self.daily_user],
|
||||
],
|
||||
)
|
||||
self.assertTableState(
|
||||
RealmCount,
|
||||
["value", "subgroup", "realm"],
|
||||
[
|
||||
[3, "private_message"],
|
||||
[2, "public_stream", self.second_realm],
|
||||
],
|
||||
)
|
||||
self.assertTableState(
|
||||
InstallationCount,
|
||||
["value", "subgroup"],
|
||||
[
|
||||
[3, "private_message"],
|
||||
[2, "public_stream"],
|
||||
],
|
||||
)
|
||||
self.assertTableState(StreamCount, [], [])
|
||||
|
||||
def test_messages_sent_by_message_type_realm_constraint(self) -> None:
|
||||
# For single Realm
|
||||
|
||||
@@ -1416,19 +1369,19 @@ class TestLoggingCountStats(AnalyticsTestCase):
|
||||
|
||||
RemotePushDeviceToken.objects.create(
|
||||
kind=RemotePushDeviceToken.FCM,
|
||||
token=token,
|
||||
token=hex_to_b64(token),
|
||||
user_uuid=(hamlet.uuid),
|
||||
server=self.server,
|
||||
)
|
||||
RemotePushDeviceToken.objects.create(
|
||||
kind=RemotePushDeviceToken.FCM,
|
||||
token=token + "aa",
|
||||
token=hex_to_b64(token + "aa"),
|
||||
user_uuid=(hamlet.uuid),
|
||||
server=self.server,
|
||||
)
|
||||
RemotePushDeviceToken.objects.create(
|
||||
kind=RemotePushDeviceToken.APNS,
|
||||
token=token,
|
||||
token=hex_to_b64(token),
|
||||
user_uuid=str(hamlet.uuid),
|
||||
server=self.server,
|
||||
)
|
||||
|
@@ -675,15 +675,15 @@ class TestMapArrays(ZulipTestCase):
|
||||
result,
|
||||
{
|
||||
"Old desktop app": [32, 36, 39],
|
||||
"Ancient iOS app": [1, 2, 3],
|
||||
"Old iOS app": [1, 2, 3],
|
||||
"Desktop app": [2, 5, 7],
|
||||
"Old mobile app (React Native)": [1, 2, 3],
|
||||
"Mobile app (Flutter)": [2, 2, 2],
|
||||
"Mobile app (React Native)": [1, 2, 3],
|
||||
"Mobile app beta (Flutter)": [2, 2, 2],
|
||||
"Web app": [1, 2, 3],
|
||||
"Python API": [2, 4, 6],
|
||||
"SomethingRandom": [4, 5, 6],
|
||||
"GitHub webhook": [7, 7, 9],
|
||||
"Ancient Android app": [64, 63, 65],
|
||||
"Old Android app": [64, 63, 65],
|
||||
"Terminal app": [9, 10, 11],
|
||||
},
|
||||
)
|
||||
|
@@ -599,13 +599,13 @@ def client_label_map(name: str) -> str:
|
||||
if name == "ZulipTerminal":
|
||||
return "Terminal app"
|
||||
if name == "ZulipAndroid":
|
||||
return "Ancient Android app"
|
||||
return "Old Android app"
|
||||
if name == "ZulipiOS":
|
||||
return "Ancient iOS app"
|
||||
return "Old iOS app"
|
||||
if name == "ZulipMobile":
|
||||
return "Old mobile app (React Native)"
|
||||
return "Mobile app (React Native)"
|
||||
if name in ["ZulipFlutter", "ZulipMobile/flutter"]:
|
||||
return "Mobile app (Flutter)"
|
||||
return "Mobile app beta (Flutter)"
|
||||
if name in ["ZulipPython", "API: Python"]:
|
||||
return "Python API"
|
||||
if name.startswith("Zulip") and name.endswith("Webhook"):
|
||||
@@ -619,9 +619,9 @@ def rewrite_client_arrays(value_arrays: dict[str, list[int]]) -> dict[str, list[
|
||||
mapped_label = client_label_map(label)
|
||||
if mapped_label in mapped_arrays:
|
||||
for i in range(len(array)):
|
||||
mapped_arrays[mapped_label][i] += array[i]
|
||||
mapped_arrays[mapped_label][i] += value_arrays[label][i]
|
||||
else:
|
||||
mapped_arrays[mapped_label] = array.copy()
|
||||
mapped_arrays[mapped_label] = [value_arrays[label][i] for i in range(len(array))]
|
||||
return mapped_arrays
|
||||
|
||||
|
||||
|
@@ -18,480 +18,13 @@ 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 12.0
|
||||
|
||||
Feature levels 421-424 reserved for future use in 11.x maintenance
|
||||
releases.
|
||||
|
||||
## Changes in Zulip 11.0
|
||||
|
||||
**Feature level 421**
|
||||
|
||||
No changes; API feature level used for the Zulip 11.0 release.
|
||||
|
||||
**Feature level 420**
|
||||
|
||||
* [`POST /mobile_push/e2ee/test_notification`](/api/e2ee-test-notify):
|
||||
Added a new endpoint to send an end-to-end encrypted test push notification
|
||||
to the user's selected mobile device or all of their mobile devices.
|
||||
|
||||
**Feature level 419**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added `simplified_presence_events`
|
||||
[client capability](/api/register-queue#parameter-client_capabilities),
|
||||
which allows clients to specify whether they support receiving the
|
||||
`presence` event type with user presence data in the modern API format.
|
||||
* [`GET /events`](/api/get-events): Added the `presences` field to the
|
||||
`presence` event type for clients that support the `simplified_presence_events`
|
||||
[client capability](/api/register-queue#parameter-client_capabilities).
|
||||
The `presences` field will have the user presence data in the modern
|
||||
API format. For clients that don't support that client capability the
|
||||
event will contain fields with the legacy format for user presence data.
|
||||
|
||||
**Feature level 418**
|
||||
|
||||
* [`GET /events`](/api/get-events): An event with `type: "channel_folder"`
|
||||
and `op: "reorder"` is sent when channel folders are reordered.
|
||||
|
||||
**Feature level 417**
|
||||
|
||||
* [`POST channels/create`](/api/create-channel): Added a dedicated
|
||||
endpoint for creating a new channel. Previously, channel creation
|
||||
was done entirely through
|
||||
[`POST /users/me/subscriptions`](/api/subscribe).
|
||||
|
||||
**Feature level 416**
|
||||
|
||||
* [`POST /invites`](/api/send-invites), [`POST
|
||||
/invites/multiuse`](/api/create-invite-link): Added a new parameter
|
||||
`welcome_message_custom_text` which allows the users to add a
|
||||
Welcome Bot custom message for new users through invitations.
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`POST /events`](/api/get-events),
|
||||
`PATCH /realm`: Added `welcome_message_custom_text` realm setting which is the
|
||||
default custom message for the Welcome Bot when sending invitations to new users.
|
||||
|
||||
* [`POST /realm/test_welcome_bot_custom_message`](/api/test-welcome-bot-custom-message):
|
||||
Added new endpoint test messages with the Welcome Bot custom message. The test
|
||||
messages are sent to the acting administrator, allowing them to preview how the
|
||||
custom welcome message will appear to new users upon joining the organization.
|
||||
|
||||
**Feature level 415**
|
||||
|
||||
* [`POST /reminders`](/api/create-message-reminder): Added parameter
|
||||
`note` to allow users to add notes to their reminders.
|
||||
* [`POST /register`](/api/register-queue): Added `max_reminder_note_length`
|
||||
for clients to restrict the reminder note length before sending it to
|
||||
the server.
|
||||
|
||||
**Feature level 414**
|
||||
|
||||
* [`POST /channel_folders/create`](/api/create-channel-folder),
|
||||
[`GET /channel_folders`](/api/get-channel-folders),
|
||||
[`PATCH /channel_folders/{channel_folder_id}`](/api/update-channel-folder):
|
||||
Added a new field `order` to show in which order should channel folders be
|
||||
displayed. The list is 0-indexed and works similar to the `order` field of
|
||||
custom profile fields.
|
||||
* [`PATCH /channel_folders`](/api/patch-channel-folders): Added a new
|
||||
endpoint for reordering channel folders. It accepts an array of channel
|
||||
folder IDs arranged in the order the user desires it to be in.
|
||||
* [`GET /channel_folders`](/api/get-channel-folders): Channel folders will
|
||||
be ordered by the `order` field instead of `id` field when being returned.
|
||||
|
||||
**Feature level 413**
|
||||
|
||||
* Mobile push notification payloads for APNs no longer contain the
|
||||
`server` and `realm_id` fields, which were unused.
|
||||
* Mobile push notification payloads for FCM to remove push
|
||||
notifications no longer contain the legacy pre-2019
|
||||
`zulip_message_id` field; all functional clients support the newer
|
||||
`zulip_message_ids`.
|
||||
* Mobile push notification payloads for FCM to for new messages no
|
||||
longer contain the (unused) `content_truncated` boolean field.
|
||||
- E2EE mobile push notification payloads now have a [modernized and
|
||||
documented format](/api/mobile-notifications).
|
||||
|
||||
**Feature level 412**
|
||||
|
||||
* [`POST /register`](/api/register-queue),
|
||||
[`GET /users/me/subscriptions`](/api/get-subscriptions):
|
||||
Added support for passing `partial` as argument to `include_subscribers`
|
||||
parameter to get only partial subscribers data of the channel.
|
||||
* [`POST /register`](/api/register-queue),
|
||||
[`GET /users/me/subscriptions`](/api/get-subscriptions):
|
||||
Added `partial_subscribers` field in `subscription` objects.
|
||||
|
||||
**Feature level 411**
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings),
|
||||
[`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults):
|
||||
Added new `web_left_sidebar_show_channel_folders` display setting,
|
||||
controlling whether any [channel folders](/help/channel-folders)
|
||||
configured by the organization are displayed in the left sidebar.
|
||||
|
||||
**Feature level 410**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added
|
||||
`max_channel_folder_name_length` and
|
||||
`max_channel_folder_description_length` fields to the response.
|
||||
* Mobile push notification payloads for APNs no longer contain the
|
||||
`time` field, which was unused.
|
||||
|
||||
**Feature level 409**
|
||||
|
||||
* `PATCH /realm`, [`POST /register`](/api/register-queue),
|
||||
[`GET /events`](/api/get-events): Added a new
|
||||
`require_e2ee_push_notifications` realm setting.
|
||||
|
||||
**Feature level 407**
|
||||
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions),
|
||||
[`GET /streams`](/api/get-streams), [`GET /events`](/api/get-events),
|
||||
[`POST /register`](/api/register-queue): Added `can_delete_any_message_group`
|
||||
field which is a [group-setting value](/api/group-setting-values) describing the
|
||||
set of users with permissions to delete any message in the channel.
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe),
|
||||
[`PATCH /streams/{stream_id}`](/api/update-stream): Added
|
||||
`can_delete_any_message_group` parameter to support setting and
|
||||
changing the user group whose members can delete any message in the specified
|
||||
channel.
|
||||
* `PATCH /realm`, [`POST /register`](/api/register-queue),
|
||||
[`GET /events`](/api/get-events): Added `can_set_delete_message_policy_group`
|
||||
realm setting, which is a [group-setting value](/api/group-setting-values)
|
||||
describing the set of users with permission to change per-channel
|
||||
`can_delete_any_message_group` and `can_delete_own_message_group` settings.
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions),
|
||||
[`GET /streams`](/api/get-streams), [`GET /events`](/api/get-events),
|
||||
[`POST /register`](/api/register-queue): Added `can_delete_own_message_group`
|
||||
field which is a [group-setting value](/api/group-setting-values) describing the
|
||||
set of users with permissions to delete the messages they have sent in the channel.
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe),
|
||||
[`PATCH /streams/{stream_id}`](/api/update-stream): Added
|
||||
`can_delete_own_message_group` parameter to support setting and
|
||||
changing the user group whose members can delete the messages they have sent
|
||||
in the channel.
|
||||
- [`POST /users/{user_id}/status`](/api/update-status-for-user): Added
|
||||
new API endpoint for an administrator to update the status for
|
||||
another user.
|
||||
|
||||
**Feature level 406**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added `push_devices`
|
||||
field to response.
|
||||
* [`GET /events`](/api/get-events): A `push_device` event is sent
|
||||
to clients when registration to bouncer either succeeds or fails.
|
||||
* [`POST /mobile_push/register`](/api/register-push-device): Added
|
||||
an endpoint to register a device to receive end-to-end encrypted
|
||||
mobile push notifications.
|
||||
|
||||
**Feature level 405**
|
||||
|
||||
* [Message formatting](/api/message-formatting): Added new HTML
|
||||
formatting for uploaded audio files generating a player experience.
|
||||
|
||||
**Feature level 404**
|
||||
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions),
|
||||
[`GET /streams`](/api/get-streams), [`GET /events`](/api/get-events),
|
||||
[`POST /register`](/api/register-queue): Added new `"empty_topic_only"`
|
||||
option to the `topics_policy` field on Stream and Subscription
|
||||
objects.
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe),
|
||||
[`PATCH /streams/{stream_id}`](/api/update-stream): Added new
|
||||
`"empty_topic_only"` option to `topics_policy` parameter for
|
||||
["general chat" channels](/help/general-chat-channels).
|
||||
|
||||
**Feature level 403**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added a `url_options` object
|
||||
to the `realm_incoming_webhook_bots` object for incoming webhook
|
||||
integration URL parameter options. Previously, these optional URL
|
||||
parameters were included in the `config_options` field (see feature
|
||||
level 318 entry). The `config_options` object is now reserved for
|
||||
configuration data that can be set when creating an bot user for a
|
||||
specific incoming webhook integration.
|
||||
|
||||
**Feature level 402**
|
||||
|
||||
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions),
|
||||
[`GET /streams`](/api/get-streams), [`GET /events`](/api/get-events),
|
||||
[`POST /register`](/api/register-queue): Added `can_resolve_topics_group`
|
||||
which is a [group-setting value](/api/group-setting-values) describing the
|
||||
set of users with permissions to resolve topics in the channel.
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe),
|
||||
[`PATCH /streams/{stream_id}`](/api/update-stream): Added `can_resolve_topics_group`
|
||||
which is a [group-setting value](/api/group-setting-values) describing the
|
||||
set of users with permissions to resolve topics in the channel.
|
||||
|
||||
**Feature level 401**
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`PATCH
|
||||
/settings`](/api/update-settings), [`PATCH
|
||||
/realm/user_settings_defaults`](/api/update-realm-user-settings-defaults):
|
||||
Added new option in user setting `web_channel_default_view`, to navigate
|
||||
to top unread topic in channel.
|
||||
|
||||
**Feature level 400**
|
||||
|
||||
* [Markdown message formatting](/api/message-formatting#links-to-channels-topics-and-messages):
|
||||
The server now prefers the latest message in a topic, not the
|
||||
oldest, when constructing topic permalinks using the `/with/` operator.
|
||||
|
||||
**Feature level 399**
|
||||
|
||||
* [`GET /events`](/api/get-events):
|
||||
Added `reminders` events sent to clients when a user creates
|
||||
or deletes scheduled messages.
|
||||
* [`GET /reminders`](/api/get-reminders):
|
||||
Clients can now request `/reminders` endpoint to fetch all
|
||||
scheduled reminders.
|
||||
* [`DELETE /reminders/{reminder_id}`](/api/delete-reminder):
|
||||
Clients can now delete a scheduled reminder.
|
||||
|
||||
**Feature level 398**
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings),
|
||||
[`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults):
|
||||
Added new `web_left_sidebar_unreads_count_summary` display setting,
|
||||
controlling whether summary unread counts are displayed in the left sidebar.
|
||||
|
||||
**Feature level 397**
|
||||
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe): Added parameter
|
||||
`send_new_subscription_messages` which determines whether the user
|
||||
would like Notification Bot to notify other users who the request
|
||||
adds to a channel.
|
||||
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe): Added
|
||||
`new_subscription_messages_sent` to the response, which is only
|
||||
present if `send_new_subscription_messages` was `true` in the request.
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added `max_bulk_new_subscription_messages`
|
||||
to the response.
|
||||
|
||||
**Feature level 396**
|
||||
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions),
|
||||
[`GET /streams`](/api/get-streams), [`GET /events`](/api/get-events),
|
||||
[`POST /register`](/api/register-queue): Added `can_move_messages_within_channel_group`
|
||||
field which is a [group-setting value](/api/group-setting-values) describing the
|
||||
set of users with permissions to move messages within the channel.
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe),
|
||||
[`PATCH /streams/{stream_id}`](/api/update-stream): Added
|
||||
`can_move_messages_within_channel_group` parameter to support setting and
|
||||
changing the user group whose members can move messages within the specified
|
||||
channel.
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions),
|
||||
[`GET /streams`](/api/get-streams), [`GET /events`](/api/get-events),
|
||||
[`POST /register`](/api/register-queue): Added `can_move_messages_out_of_channel_group`
|
||||
field which is a [group-setting value](/api/group-setting-values) describing the
|
||||
set of users with permissions to move messages out of the channel.
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe),
|
||||
[`PATCH /streams/{stream_id}`](/api/update-stream): Added
|
||||
`can_move_messages_out_of_channel_group` parameter to support setting and
|
||||
changing the user group whose members can move messages out of the specified
|
||||
channel.
|
||||
|
||||
**Feature level 395**
|
||||
|
||||
* [Markdown message
|
||||
formatting](/api/message-formatting#removed-features): Previously,
|
||||
Zulip's Markdown syntax had special support for previewing Dropbox
|
||||
albums. Dropbox albums no longer exist, and links to Dropbox folders
|
||||
now consistently use Zulip's standard open graph preview markup.
|
||||
|
||||
**Feature level 394**
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`GET
|
||||
/events`](/api/get-events), [`GET /streams`](/api/get-streams),
|
||||
[`GET /streams/{stream_id}`](/api/get-stream-by-id): Added a new
|
||||
field `subscriber_count` to Stream and Subscription objects with the
|
||||
total number of non-deactivated users who are subscribed to the
|
||||
channel.
|
||||
|
||||
**Feature level 393**
|
||||
|
||||
* [`PATCH /messages/{message_id}`](/api/delete-message),
|
||||
[`POST /register`](/api/register-queue), [`GET /events`](/api/get-events):
|
||||
In `delete_message` event, all the `message_ids` will now be sorted in
|
||||
increasing order.
|
||||
* [`PATCH /messages/{message_id}`](/api/update-message),
|
||||
[`POST /register`](/api/register-queue), [`GET /events`](/api/get-events):
|
||||
In `update_message` event, all the `message_ids` will now be sorted in
|
||||
increasing order.
|
||||
|
||||
**Feature level 392**
|
||||
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions),
|
||||
[`GET /streams`](/api/get-streams), [`GET /events`](/api/get-events),
|
||||
[`POST /register`](/api/register-queue): Added the `topics_policy`
|
||||
field to Stream and Subscription objects to support channel-level
|
||||
configurations for sending messages to the empty ["general chat"
|
||||
topic](/help/general-chat-topic).
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe),
|
||||
[`PATCH /streams/{stream_id}`](/api/update-stream): Added
|
||||
`topics_policy` parameter to support setting and updating the
|
||||
channel-level configuration for sending messages to the
|
||||
empty ["general chat" topic](/help/general-chat-topic).
|
||||
* `PATCH /realm`, [`GET /events`](/api/get-events),
|
||||
[`POST /register`](/api/register-queue): Added
|
||||
`can_set_topics_policy_group` realm setting, which is a
|
||||
[group-setting value](/api/group-setting-values) describing the set
|
||||
of users with permission to change the per-channel `topics_policy`
|
||||
setting.
|
||||
* `PATCH /realm`, [`GET /events`](/api/get-events),
|
||||
[`POST /register`](/api/register-queue):
|
||||
Added a new realm `topics_policy` setting for the organization's
|
||||
default policy for sending channel messages to the empty ["general
|
||||
chat" topic](/help/general-chat-topic).
|
||||
* [`GET /events`](/api/get-events), [`POST /register`](/api/register-queue):
|
||||
Deprecated the realm `mandatory_topics` setting in favor of the new
|
||||
realm `topics_policy` setting.
|
||||
* `PATCH /realm`: Removed the `mandatory_topics` parameter as it is now
|
||||
replaced by the realm `topics_policy` setting.
|
||||
|
||||
**Feature level 391**
|
||||
|
||||
* [`POST /user_groups/{user_group_id}/members`](/api/update-user-group-members),
|
||||
[`POST /user_groups/{user_group_id}/subgroups`](/api/update-user-group-subgroups):
|
||||
Adding/removing members and subgroups to a deactivated group is now allowed.
|
||||
|
||||
**Feature level 390**
|
||||
|
||||
* [`GET /events`](/api/get-events): Events with `type: "navigation_view"` are
|
||||
sent to the user when a navigation view is created, updated, or removed.
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added `navigation_views` field in
|
||||
response.
|
||||
|
||||
* [`GET /navigation_views`](/api/get-navigation-views): Added a new endpoint for
|
||||
fetching all navigation views of the user.
|
||||
|
||||
* [`POST /navigation_views`](/api/add-navigation-view): Added a new endpoint for
|
||||
creating a new navigation view.
|
||||
|
||||
* [`PATCH /navigation_views/{fragment}`](/api/edit-navigation-view): Added a new
|
||||
endpoint for editing the details of a navigation view.
|
||||
|
||||
* [`DELETE /navigation_views/{fragment}`](/api/remove-navigation-view): Added a new
|
||||
endpoint for removing a navigation view.
|
||||
|
||||
**Feature level 389**
|
||||
|
||||
* [`POST /channel_folders/create`](/api/create-channel-folder): Added
|
||||
a new endpoint for creating a new channel folder.
|
||||
* [`GET /channel_folders`](/api/get-channel-folders): Added a new endpoint
|
||||
to get all channel folders in the realm.
|
||||
* [`PATCH /channel_folders/{channel_folder_id}`](/api/update-channel-folder):
|
||||
Added a new endpoint to update channel folder.
|
||||
* [`POST /register`](/api/register-queue): Added `channel_folders` field to
|
||||
response.
|
||||
* [`GET /events`](/api/get-events): An event with `type: "channel_folder"` is
|
||||
sent to all users when a channel folder is created.
|
||||
* [`GET /users/me/subscriptions`](/api/get-subscriptions),
|
||||
[`GET /streams`](/api/get-streams), [`GET /events`](/api/get-events),
|
||||
[`POST /register`](/api/register-queue): Added `folder_id` field
|
||||
to Stream and Subscription objects.
|
||||
* [`POST /users/me/subscriptions`](/api/subscribe): Added support to add
|
||||
newly created channels to folder using `folder_id` parameter.
|
||||
* [`PATCH /streams/{stream_id}`](/api/update-stream): Added support
|
||||
for updating folder to which the channel belongs.
|
||||
* [`GET /events`](/api/get-events): An event with `type: "channel_folder"` is
|
||||
sent to all users when a channel folder is updated.
|
||||
* [`GET /events`](/api/get-events): `value` field in `stream/update`
|
||||
events can have `null` when channel is removed from a folder.
|
||||
|
||||
**Feature level 388**
|
||||
|
||||
* [`PATCH /streams/{stream_id}`](/api/update-stream): Added
|
||||
`is_archived` parameter to support unarchiving previously archived
|
||||
channels.
|
||||
|
||||
**Feature level 387**
|
||||
|
||||
* [`GET /users`](/api/get-users): This endpoint no longer requires
|
||||
authentication for organizations using the [public access
|
||||
option](/help/public-access-option).
|
||||
|
||||
**Feature level 386**
|
||||
|
||||
* [`PATCH /user_groups/{user_group_id}`](/api/update-user-group):
|
||||
Added support to reactivate groups by passing `deactivated`
|
||||
parameter as `False`.
|
||||
|
||||
**Feature level 385**
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`PATCH/settings`](/api/update-settings),
|
||||
[`PATCH/realm/user_settings_defaults`](/api/update-realm-user-settings-defaults):
|
||||
Added new `resolved_topic_notice_auto_read_policy` setting, which controls
|
||||
how resolved-topic notices are marked as read for a user.
|
||||
|
||||
**Feature level 384**
|
||||
|
||||
* [`GET /users`](/api/get-users): Added `user_ids` query parameter to
|
||||
fetch data only for the provided `user_ids`.
|
||||
|
||||
**Feature level 383**
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`PATCH
|
||||
/settings`](/api/update-settings), [`PATCH
|
||||
/realm/user_settings_defaults`](/api/update-realm-user-settings-defaults):
|
||||
Added new option in user setting `web_channel_default_view`, to show
|
||||
inbox view style list of topics.
|
||||
|
||||
**Feature level 382**
|
||||
|
||||
* `POST /message/{message_id}/report`: Added a new endpoint for submitting
|
||||
a moderation request for a message.
|
||||
|
||||
**Feature level 381**
|
||||
|
||||
* [`POST /reminders`](/api/create-message-reminder): Added a new endpoint to
|
||||
schedule personal reminder for a message.
|
||||
|
||||
**Feature level 380**
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`GET
|
||||
/events`](/api/get-events): The `is_moderator` convenience field now
|
||||
is true for organization administrators, matching how `is_admin`
|
||||
works for organization owners.
|
||||
|
||||
**Feature level 379**
|
||||
|
||||
* [`PATCH /messages/{message_id}`](/api/update-message): Added
|
||||
optional parameter `prev_content_sha256`, which clients can use to
|
||||
prevent races with the message being edited by another client.
|
||||
|
||||
**Feature level 378**
|
||||
|
||||
* [`GET /events`](/api/get-events): Archiving and unarchiving
|
||||
streams now send `update` events to clients that declared
|
||||
the `archived_channels` client capability. `delete` and `create`
|
||||
events are still sent to clients that did not declare
|
||||
`archived_channels` client capability.
|
||||
* [`POST /register`](/api/register-queue): The `streams` data
|
||||
structure now includes archived channels for clients that
|
||||
declared the `archived_channels` client capability.
|
||||
|
||||
**Feature level 377**
|
||||
|
||||
* [`GET /events`](/api/get-events): When a user is deactivate, send
|
||||
`peer_remove` event to all the subscribers of the streams that the
|
||||
user was subscribed to.
|
||||
|
||||
Feature levels 373-376 reserved for future use in 10.x maintenance
|
||||
releases.
|
||||
|
||||
## Changes in Zulip 10.1
|
||||
## Changes in Zulip 10.0
|
||||
|
||||
**Feature level 372**
|
||||
|
||||
* [`POST /typing`](/api/set-typing-status): The `"(no topic)"` value
|
||||
when used for `topic` parameter is now interpreted as an empty string.
|
||||
|
||||
## Changes in Zulip 10.0
|
||||
|
||||
**Feature level 371**
|
||||
|
||||
No changes; feature level used for Zulip 10.0 release.
|
||||
@@ -1069,14 +602,10 @@ deactivated groups.
|
||||
|
||||
**Feature level 318**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Renamed the `config` object in the
|
||||
`realm_incoming_webhook_bots` object to `config_options`. This object now
|
||||
includes details about optional URL parameters that can be configured when
|
||||
[generating a URL](/help/generate-integration-url) for an incoming webhook
|
||||
integration. Previously, this object was reserved for key-value pairs that
|
||||
indicated that a bot user could be created with additional configuration
|
||||
data (such as an API key) for that incoming webhook integration, but this
|
||||
functionality has not been implemented for any existing integrations.
|
||||
* [`POST /register`](/api/register-queue): Updated
|
||||
`realm_incoming_webhook_bots` with a new `config_options` key,
|
||||
defining which options should be offered when creating URLs for this
|
||||
integration.
|
||||
|
||||
**Feature level 317**
|
||||
|
||||
@@ -1328,7 +857,7 @@ deactivated groups.
|
||||
* [`DELETE /saved_snippets/{saved_snippet_id}`](/api/delete-saved-snippet): Added
|
||||
a new endpoint for deleting saved snippets.
|
||||
|
||||
**Feature level 296**
|
||||
**Feature level 296**:
|
||||
|
||||
* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events),
|
||||
[`POST /realm/profile_fields`](/api/create-custom-profile-field),
|
||||
@@ -1492,10 +1021,10 @@ deactivated groups.
|
||||
now contains the superset of the true value that best approximates the actual
|
||||
permission setting.
|
||||
|
||||
Feature level 279 is reserved for future use in 9.x maintenance
|
||||
Feature levels 278-279 are reserved for future use in 9.x maintenance
|
||||
releases.
|
||||
|
||||
## Changes in Zulip 9.2
|
||||
## Changes in Zulip 9.0
|
||||
|
||||
**Feature level 278**
|
||||
|
||||
@@ -1505,8 +1034,6 @@ releases.
|
||||
(`image-loading-placeholder`), containing the dimensions of the
|
||||
original image. Backported change from feature level 287.
|
||||
|
||||
## Changes in Zulip 9.0
|
||||
|
||||
**Feature level 277**
|
||||
|
||||
No changes; feature level used for Zulip 9.0 release.
|
||||
|
@@ -42,7 +42,7 @@ Zulip API.
|
||||
|
||||
Note that many narrows, including all that lack a `channel` or `channels`
|
||||
operator, search the current user's personal message history. See
|
||||
[searching shared history](/help/search-for-messages#search-shared-history)
|
||||
[searching shared history](/help/search-for-messages#searching-shared-history)
|
||||
for details.
|
||||
|
||||
Clients should note that the `is:unread` filter takes advantage of the
|
||||
@@ -58,7 +58,7 @@ as an empty string.
|
||||
|
||||
## Changes
|
||||
|
||||
* In Zulip 10.0 (feature level 366), support was added for a new
|
||||
* In Zulip 10.0 (feature level ZF-f80735), support was added for a new
|
||||
`is:muted` operator combination, matching messages in topics and
|
||||
channels that the user has [muted](/help/mute-a-topic).
|
||||
|
||||
|
@@ -42,7 +42,7 @@ Botserver interaction are:
|
||||
@**My Bot User** hello world
|
||||
```
|
||||
|
||||
1. The Zulip server sends a POST request to your Botserver endpoint URL:
|
||||
1. The Zulip server sends a POST request to the Botserver on `https://bot-server.example.com/`:
|
||||
|
||||
```
|
||||
{
|
||||
@@ -60,7 +60,9 @@ Botserver interaction are:
|
||||
1. The Botserver searches for a bot to handle the message, and executes your
|
||||
bot's `handle_message` code.
|
||||
|
||||
Your bot's code should work just like it does with `zulip-run-bot`.
|
||||
Your bot's code should work just like it does with `zulip-run-bot`;
|
||||
for example, you reply using
|
||||
[bot_handler.send_reply](writing-bots#bot_handlersend_reply)).
|
||||
|
||||
### Installing the Zulip Botserver
|
||||
|
||||
@@ -70,34 +72,30 @@ Install the `zulip_botserver` package:
|
||||
pip3 install zulip_botserver
|
||||
```
|
||||
|
||||
### Create a bot in your Zulip organization
|
||||
|
||||
{start_tabs}
|
||||
|
||||
1. Navigate to the **Bots** tab of the **Personal settings** menu, and click
|
||||
**Add a new bot**.
|
||||
|
||||
1. Set the **Bot type** to **Outgoing webhook**.
|
||||
|
||||
1. Set the **endpoint URL** to `https://<host>:<port>` where `host` is the
|
||||
hostname of the server you'll be running the Botserver on, and `port` is
|
||||
the port number. The default port is `5002`.
|
||||
|
||||
1. Click **Create bot**. You should see the new bot user in the
|
||||
**Active bots** panel.
|
||||
|
||||
{end_tabs}
|
||||
|
||||
### Running a bot using the Zulip Botserver
|
||||
|
||||
{start_tabs}
|
||||
|
||||
1. [Create your bot](#create-a-bot-in-your-zulip-organization) in your Zulip
|
||||
organization.
|
||||
1. Construct the URL for your bot, which will be of the form:
|
||||
|
||||
1. Download the `zuliprc` file for the bot created above from the
|
||||
**Bots** tab of the **Personal settings** menu, by clicking the download
|
||||
(<i class="fa fa-download"></i>) icon under the bot's name.
|
||||
```
|
||||
http://<hostname>:<port>
|
||||
```
|
||||
|
||||
where the `hostname` is the hostname you'll be running the bot
|
||||
server on, and `port` is the port for it (the recommended default
|
||||
is `5002`).
|
||||
|
||||
1. Register new bot users on the Zulip server's web interface.
|
||||
|
||||
* Log in to the Zulip server.
|
||||
* Navigate to *Personal settings (<i class="zulip-icon zulip-icon-gear"></i>)* -> *Bots* -> *Add a new bot*.
|
||||
Select *Outgoing webhook* for bot type, fill out the form (using
|
||||
the URL from above) and click on *Create bot*.
|
||||
* A new bot user should appear in the *Active bots* panel.
|
||||
|
||||
1. Download the `zuliprc` file for your bot from the *Active Bots*
|
||||
panel, using the download button.
|
||||
|
||||
1. Run the Botserver, where `helloworld` is the name of the bot you
|
||||
want to run:
|
||||
@@ -107,27 +105,36 @@ pip3 install zulip_botserver
|
||||
You can specify the port number and various other options; run
|
||||
`zulip-botserver --help` to see how to do this.
|
||||
|
||||
{end_tabs}
|
||||
1. Congrats, everything is set up! Test your Botserver like you would
|
||||
test a normal bot.
|
||||
|
||||
Congrats, everything is set up! Test your Botserver like you would
|
||||
test a normal bot.
|
||||
{end_tabs}
|
||||
|
||||
### Running multiple bots using the Zulip Botserver
|
||||
|
||||
The Zulip Botserver also supports running multiple bots from a single
|
||||
Botserver process.
|
||||
Botserver process. You can do this with the following procedure.
|
||||
|
||||
{start_tabs}
|
||||
|
||||
1. [Create your bots](#create-a-bot-in-your-zulip-organization)
|
||||
in your Zulip organization.
|
||||
1. Download the `botserverrc` from the `your-bots` settings page, using
|
||||
the "Download config of all active outgoing webhook bots in Zulip
|
||||
Botserver format." option at the top.
|
||||
|
||||
1. Download the `botserverrc` file from the **Bots** tab of the
|
||||
**Personal settings** menu, using the **Download config of all active
|
||||
outgoing webhook bots in Zulip Botserver format** option.
|
||||
1. Open the `botserverrc`. It should contain one or more sections that look like this:
|
||||
|
||||
1. Open the `botserverrc`. It should contain one or more sections that look
|
||||
like this:
|
||||
```
|
||||
[]
|
||||
email=foo-bot@hostname
|
||||
key=dOHHlyqgpt5g0tVuVl6NHxDLlc9eFRX4
|
||||
site=http://hostname
|
||||
token=aQVQmSd6j6IHphJ9m1jhgHdbnhl5ZcsY
|
||||
```
|
||||
|
||||
Each section contains the configuration for an outgoing webhook bot. For each
|
||||
bot, enter the name of the bot you want to run in the square brackets `[]`.
|
||||
For example, if we want `foo-bot@hostname` to run the `helloworld` bot, our
|
||||
new section would look like this:
|
||||
|
||||
```
|
||||
[helloworld]
|
||||
@@ -135,25 +142,24 @@ Botserver process.
|
||||
key=dOHHlyqgpt5g0tVuVl6NHxDLlc9eFRX4
|
||||
site=http://hostname
|
||||
token=aQVQmSd6j6IHphJ9m1jhgHdbnhl5ZcsY
|
||||
bot-config-file=~/path/to/helloworld.conf
|
||||
```
|
||||
|
||||
Each section contains the configuration for an outgoing webhook bot.
|
||||
To run an external bot, enter the path to the bot's python file in the square
|
||||
brackets `[]`. For example, if we want to run `~/Documents/my_new_bot.py`, our
|
||||
new section could look like this:
|
||||
|
||||
1. For each bot, enter the name of the bot you want to run in the square
|
||||
brackets `[]`, e.g., the above example applies to the `helloworld` bot.
|
||||
To run an external bot, enter the path to the bot's python file instead,
|
||||
e.g., `[~/Documents/my_bot_script.py]`.
|
||||
```
|
||||
[~/Documents/my_new_bot.py]
|
||||
email=foo-bot@hostname
|
||||
key=dOHHlyqgpt5g0tVuVl6NHxDLlc9eFRX4
|
||||
site=http://hostname
|
||||
```
|
||||
|
||||
!!! tip ""
|
||||
|
||||
The `bot-config-file` setting is needed only for bots that
|
||||
use a config file.
|
||||
|
||||
1. Run the Zulip Botserver by passing the `botserverrc` to it.
|
||||
1. Run the Zulip Botserver by passing the `botserverrc` to it. The
|
||||
command format is:
|
||||
|
||||
```
|
||||
zulip-botserver --config-file <path-to-botserverrc> --hostname <address> --port <port>
|
||||
zulip-botserver --config-file <path_to_botserverrc>
|
||||
```
|
||||
|
||||
If omitted, `hostname` defaults to `127.0.0.1` and `port` to `5002`.
|
||||
|
@@ -48,7 +48,7 @@ client = zulip.Client(
|
||||
|
||||
If you are working on an integration that you plan to share outside
|
||||
your organization, you can get help picking a good name in
|
||||
[#integrations][integrations-channel] in the [Zulip development
|
||||
`#integrations` in the [Zulip development
|
||||
community](https://zulip.com/development-community/).
|
||||
|
||||
## Rate-limiting response headers
|
||||
@@ -78,4 +78,3 @@ to a given request, the values returned will be for the strictest
|
||||
limit.
|
||||
|
||||
[rate-limiting-rules]: https://zulip.readthedocs.io/en/latest/production/security-model.html#rate-limiting
|
||||
[integrations-channel]: https://chat.zulip.org/#narrow/channel/127-integrations/
|
||||
|
@@ -18,7 +18,6 @@
|
||||
* [Mark messages in a channel as read](/api/mark-stream-as-read)
|
||||
* [Mark messages in a topic as read](/api/mark-topic-as-read)
|
||||
* [Get a message's read receipts](/api/get-read-receipts)
|
||||
* [Report a message](/api/report-message)
|
||||
|
||||
#### Scheduled messages
|
||||
|
||||
@@ -27,12 +26,6 @@
|
||||
* [Edit a scheduled message](/api/update-scheduled-message)
|
||||
* [Delete a scheduled message](/api/delete-scheduled-message)
|
||||
|
||||
#### Message reminders
|
||||
|
||||
* [Create a message reminder](/api/create-message-reminder)
|
||||
* [Get reminders](/api/get-reminders)
|
||||
* [Delete a reminder](/api/delete-reminder)
|
||||
|
||||
#### Drafts
|
||||
|
||||
* [Get drafts](/api/get-drafts)
|
||||
@@ -44,13 +37,6 @@
|
||||
* [Edit a saved snippet](/api/edit-saved-snippet)
|
||||
* [Delete a saved snippet](/api/delete-saved-snippet)
|
||||
|
||||
#### Navigation views
|
||||
|
||||
* [Get all navigation views](/api/get-navigation-views)
|
||||
* [Add a navigation view](/api/add-navigation-view)
|
||||
* [Update the navigation view](/api/edit-navigation-view)
|
||||
* [Remove a navigation view](/api/remove-navigation-view)
|
||||
|
||||
#### Channels
|
||||
|
||||
* [Get subscribed channels](/api/get-subscriptions)
|
||||
@@ -62,7 +48,7 @@
|
||||
* [Get all channels](/api/get-streams)
|
||||
* [Get a channel by ID](/api/get-stream-by-id)
|
||||
* [Get channel ID](/api/get-stream-id)
|
||||
* [Create a channel](/api/create-channel)
|
||||
* [Create a channel](/api/create-stream)
|
||||
* [Update a channel](/api/update-stream)
|
||||
* [Archive a channel](/api/archive-stream)
|
||||
* [Get channel's email address](/api/get-stream-email-address)
|
||||
@@ -72,17 +58,13 @@
|
||||
* [Delete a topic](/api/delete-topic)
|
||||
* [Add a default channel](/api/add-default-stream)
|
||||
* [Remove a default channel](/api/remove-default-stream)
|
||||
* [Create a channel folder](/api/create-channel-folder)
|
||||
* [Get channel folders](/api/get-channel-folders)
|
||||
* [Reorder channel folders](/api/patch-channel-folders)
|
||||
* [Update a channel folder](/api/update-channel-folder)
|
||||
|
||||
#### Users
|
||||
|
||||
* [Get a user](/api/get-user)
|
||||
* [Get a user by email](/api/get-user-by-email)
|
||||
* [Get own user](/api/get-own-user)
|
||||
* [Get users](/api/get-users)
|
||||
* [Get all users](/api/get-users)
|
||||
* [Create a user](/api/create-user)
|
||||
* [Update a user](/api/update-user)
|
||||
* [Update a user by email](/api/update-user-by-email)
|
||||
@@ -91,7 +73,6 @@
|
||||
* [Reactivate a user](/api/reactivate-user)
|
||||
* [Get a user's status](/api/get-user-status)
|
||||
* [Update your status](/api/update-status)
|
||||
* [Update user status](/api/update-status-for-user)
|
||||
* [Set "typing" status](/api/set-typing-status)
|
||||
* [Set "typing" status for message editing](/api/set-typing-status-for-message-edit)
|
||||
* [Get a user's presence](/api/get-user-presence)
|
||||
@@ -144,7 +125,6 @@
|
||||
* [Get all data exports](/api/get-realm-exports)
|
||||
* [Create a data export](/api/export-realm)
|
||||
* [Get data export consent state](/api/get-realm-export-consents)
|
||||
* [Test welcome bot custom message](/api/test-welcome-bot-custom-message)
|
||||
|
||||
#### Real-time events
|
||||
|
||||
@@ -157,9 +137,6 @@
|
||||
|
||||
* [Fetch an API key (production)](/api/fetch-api-key)
|
||||
* [Fetch an API key (development only)](/api/dev-fetch-api-key)
|
||||
* [Send an E2EE test notification to mobile device(s)](/api/e2ee-test-notify)
|
||||
* [Register E2EE push device](/api/register-push-device)
|
||||
* [Mobile notifications](/api/mobile-notifications)
|
||||
* [Send a test notification to mobile device(s)](/api/test-notify)
|
||||
* [Add an APNs device token](/api/add-apns-token)
|
||||
* [Remove an APNs device token](/api/remove-apns-token)
|
||||
|
@@ -210,108 +210,26 @@ tools which you can use to test your webhook - 2 command line tools and a GUI.
|
||||
|
||||
### Webhooks requiring custom configuration
|
||||
|
||||
In cases where an incoming webhook integration supports optional URL parameters,
|
||||
one can use the `url_options` feature. It's a field in the `WebhookIntegration`
|
||||
class that is used when [generating a URL for an integration](/help/generate-integration-url)
|
||||
in the web app, which encodes the user input for each URL parameter in the
|
||||
incoming webhook's URL.
|
||||
In rare cases, it's necessary for an incoming webhook to require
|
||||
additional user configuration beyond what is specified in the post
|
||||
URL. The typical use case for this is APIs like the Stripe API that
|
||||
require clients to do a callback to get details beyond an opaque
|
||||
object ID that one would want to include in a Zulip notification.
|
||||
|
||||
These URL options are declared as follows:
|
||||
These configuration options are declared as follows:
|
||||
|
||||
```python
|
||||
WebhookIntegration(
|
||||
'helloworld',
|
||||
...
|
||||
url_options=[
|
||||
WebhookUrlOption(
|
||||
name='ignore_private_repositories',
|
||||
label='Exclude notifications from private repositories',
|
||||
validator=check_string
|
||||
),
|
||||
],
|
||||
)
|
||||
WebhookIntegration('helloworld', ['misc'], display_name='Hello World',
|
||||
config_options=[('HelloWorld API key', 'hw_api_key', check_string)])
|
||||
```
|
||||
|
||||
`url_options` is a list describing the parameters the web app UI should offer when
|
||||
generating the incoming webhook URL:
|
||||
`config_options` is a list describing the parameters the user should
|
||||
configure:
|
||||
1. A user-facing string describing the field to display to users.
|
||||
2. The field name you'll use to access this from your `view.py` function.
|
||||
3. A Validator, used to verify the input is valid.
|
||||
|
||||
- `name`: The parameter name that is used to encode the user input in the
|
||||
integration's webhook URL.
|
||||
- `label`: A short descriptive label for this URL parameter in the web app UI.
|
||||
- `validator`: A validator function, which is used to determine the input type
|
||||
for this option in the UI, and to indicate how to validate the input.
|
||||
Currently, the web app UI only supports these validators:
|
||||
- `check_bool` for checkbox/select input.
|
||||
- `check_string` for text input.
|
||||
|
||||
!!! warn ""
|
||||
|
||||
**Note**: To add support for other validators, you can update
|
||||
`web/src/integration_url_modal.ts`. Common validators are available in
|
||||
`zerver/lib/validator.py`.
|
||||
|
||||
In rare cases, it may be necessary for an incoming webhook to require
|
||||
additional user configuration beyond what is specified in the POST
|
||||
URL. A typical use case for this would be APIs that require clients
|
||||
to do a callback to get details beyond an opaque object ID that one
|
||||
would want to include in a Zulip notification message.
|
||||
|
||||
The `config_options` field in the `WebhookIntegration` class is reserved
|
||||
for this use case.
|
||||
|
||||
### WebhookUrlOption presets
|
||||
|
||||
The `build_preset_config` method creates `WebhookUrlOption` objects with
|
||||
pre-configured fields. These preset URL options primarily serve two
|
||||
purposes:
|
||||
|
||||
- To construct common `WebhookUrlOption` objects that are used in various
|
||||
incoming webhook integrations.
|
||||
|
||||
- To construct `WebhookUrlOption` objects with special UI in the web-app
|
||||
for [generating incoming webhook URLs](/help/generate-integration-url).
|
||||
|
||||
Using a preset URL option with the `build_preset_config` method:
|
||||
|
||||
```python
|
||||
# zerver/lib/integrations.py
|
||||
from zerver.lib.webhooks.common import PresetUrlOption, WebhookUrlOption
|
||||
# -- snip --
|
||||
WebhookIntegration(
|
||||
"github",
|
||||
# -- snip --
|
||||
url_options=[
|
||||
WebhookUrlOption.build_preset_config(PresetUrlOption.BRANCHES),
|
||||
],
|
||||
),
|
||||
```
|
||||
|
||||
Currently configured preset URL options:
|
||||
|
||||
- **`BRANCHES`**: This preset is intended to be used for [version control
|
||||
integrations](/integrations/version-control), and adds UI for the user to
|
||||
configure which branches of a project's repository will trigger Zulip
|
||||
notification messages. When the user specifies which branches to receive
|
||||
notifications from, the `branches` parameter will be added to the [generated
|
||||
integration URL](/help/generate-integration-url). For example, if the user
|
||||
input `main` and `dev` for the branches of their repository, then
|
||||
`&branches=main%2Cdev` would be appended to the generated integration URL.
|
||||
|
||||
- **`IGNORE_PRIVATE_REPOSITORIES`**: This preset is intended to be used for
|
||||
[version control integrations](/integrations/version-control), and adds UI
|
||||
for the user exclude private repositories from triggering Zulip
|
||||
notification messages. When the user selects this option, the
|
||||
`ignore_private_repositories` boolean parameter will be added to the
|
||||
[generated integration URL](/help/generate-integration-url).
|
||||
|
||||
- **`MAPPING`**: This preset is intended to be used for [chat-app
|
||||
integrations](/integrations/communication) (like Slack), and adds a
|
||||
special option, **Matching Zulip channel**, to the UI for where to send
|
||||
Zulip notification messages. This special option maps the notification
|
||||
messages to Zulip channels that match the messages' original channel
|
||||
name in the third-party app. When selected, this requires setting a
|
||||
single topic for notification messages, and adds `&mapping=channels`
|
||||
to the [generated integration URL](/help/generate-integration-url).
|
||||
Common validators are available in `zerver/lib/validators.py`.
|
||||
|
||||
## Step 4: Manually testing the webhook
|
||||
|
||||
|
@@ -17,8 +17,10 @@ If you don't find an integration you need, you can:
|
||||
pull
|
||||
request](https://zulip.readthedocs.io/en/latest/contributing/reviewable-prs.html)
|
||||
to get your integration merged into the main Zulip repository.
|
||||
|
||||
- [File an issue](https://github.com/zulip/zulip/issues/new/choose) to request
|
||||
an integration (if it's a nice-to-have).
|
||||
|
||||
- [Contact Zulip Sales](mailto:sales@zulip.com) to inquire about a custom
|
||||
development contract.
|
||||
|
||||
@@ -44,7 +46,7 @@ additional integrations through [Zapier](https://zapier.com/apps) and
|
||||
{start_tabs}
|
||||
|
||||
1. Search [Zapier](https://zapier.com/apps) or [IFTTT](https://ifttt.com/search)
|
||||
for the product you'd like to connect to Zulip.
|
||||
for the product you'd like to connect to Zulip.
|
||||
|
||||
1. Follow the integration instructions for [Zapier](/integrations/doc/zapier) or
|
||||
[IFTTT](/integrations/doc/ifttt).
|
||||
@@ -104,10 +106,13 @@ Sales](mailto:sales@zulip.com).
|
||||
|
||||
* If the third-party service supports outgoing webhooks, you likely want to
|
||||
build an [incoming webhook integration](/api/incoming-webhooks-overview).
|
||||
|
||||
* If it doesn't, you may want to write a
|
||||
[script or plugin integration](/api/non-webhook-integrations).
|
||||
|
||||
* The [`zulip-send` tool](/api/send-message) makes it easy to send Zulip
|
||||
messages from shell scripts.
|
||||
|
||||
* Finally, you can
|
||||
[send messages using Zulip's API](/api/send-message), with bindings for
|
||||
Python, JavaScript and [other languages](/api/client-libraries).
|
||||
@@ -117,6 +122,7 @@ Sales](mailto:sales@zulip.com).
|
||||
* To react to activity inside Zulip, look at Zulip's
|
||||
[Python framework for interactive bots](/api/running-bots) or
|
||||
[Zulip's real-time events API](/api/get-events).
|
||||
|
||||
* If what you want isn't covered by the above, check out the full
|
||||
[REST API](/api/rest). The web, mobile, desktop, and terminal apps are
|
||||
built on top of this API, so it can do anything a human user can do. Most
|
||||
|
@@ -62,8 +62,10 @@ The `near` and `with` operators are documented in more detail in the
|
||||
topic links with the `with` operator, the code doing the rendering may
|
||||
pick the ID arbitrarily among messages accessible to the client and/or
|
||||
acting user at the time of rendering. Currently, the server chooses
|
||||
the message ID to use for `with` operators as the latest message ID in
|
||||
the topic accessible to the user who wrote the message.
|
||||
the message ID to use for `with` operators as the oldest message ID in
|
||||
the topic accessible to the user who wrote the message. In channels
|
||||
with protected history, this means the same Markdown syntax may be
|
||||
rendered differently for users who joined at different times.
|
||||
|
||||
The older stream/topic link elements include a `data-stream-id`, which
|
||||
historically was used in order to display the current channel name if
|
||||
@@ -99,11 +101,7 @@ are as follows:
|
||||
</a>
|
||||
```
|
||||
|
||||
**Changes**: In Zulip 11.0 (feature level 400), the server switched
|
||||
its strategy for `with` URL construction to choose the latest
|
||||
accessible message ID in a topic. Previously, it used the oldest.
|
||||
|
||||
Before Zulip 10.0 (feature level 347), the `with` field
|
||||
**Changes**: Before Zulip 10.0 (feature level 347), the `with` field
|
||||
was never used in topic link URLs generated by the server; the markup
|
||||
currently used only for empty topics was used for all topic links.
|
||||
|
||||
@@ -115,12 +113,6 @@ In Zulip 10.0 (feature level 319), added Markdown syntax
|
||||
for linking to a specific message in a conversation. Declared the
|
||||
`data-stream-id` field to be deprecated as detailed above.
|
||||
|
||||
In Zulip 11.0 (feature level 383), clients can decide what
|
||||
channel view a.stream channel link elements take you to -- i.e.,
|
||||
the href for those is the default behavior of the link that also
|
||||
encodes the channel alongside the data-stream-id field, but clients
|
||||
can override that default based on `web_channel_default_view` setting.
|
||||
|
||||
## Image previews
|
||||
|
||||
When a Zulip message is sent linking to an uploaded image, Zulip will
|
||||
@@ -243,55 +235,6 @@ assume that messages sent prior to the introduction of thumbnailing
|
||||
have been re-rendered to use the new format or have thumbnails
|
||||
available.
|
||||
|
||||
## Video embeddings and previews
|
||||
|
||||
When a Zulip message is sent linking to an uploaded video, Zulip may
|
||||
generate a video preview element with the following format.
|
||||
|
||||
|
||||
``` html
|
||||
<div class="message_inline_image message_inline_video">
|
||||
<a href="/user_uploads/path/to/video.mp4">
|
||||
<video preload="metadata" src="/user_uploads/path/to/video.mp4">
|
||||
</video>
|
||||
</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Audio Players
|
||||
|
||||
When the Markdown media syntax is used with an uploaded file with an
|
||||
audio `Content-Type`, Zulip will generate an HTML5 `<audio>` player
|
||||
element. Supported MIME types are currently `audio/aac`, `audio/flac`,
|
||||
`audio/mpeg`, and `audio/wav`.
|
||||
|
||||
For example, `[file.mp3](/user_uploads/path/to/file.mp3)` renders as:
|
||||
|
||||
``` html
|
||||
<audio controls preload="metadata"
|
||||
src="/user_uploads/path/to/file.mp3" title="file.mp3">
|
||||
</audio>
|
||||
```
|
||||
|
||||
If the Zulip server has rewritten the URL of the audio file, it will
|
||||
provide the URL in a `data-original-url` parameter. The Zulip server
|
||||
does this for all non-uploaded file audio URLs.
|
||||
|
||||
``` html
|
||||
<audio controls preload="metadata"
|
||||
data-original-url="https://example.com/path/to/original/file.mp3"
|
||||
src="https://zulipcdn.example.com/path/to/playable/file.mp3" title="file.mp3">
|
||||
</audio>
|
||||
```
|
||||
|
||||
Clients that cannot render an audio player are recommended to convert
|
||||
audio elements into a link to the original URL.
|
||||
|
||||
The Zulip server does not validate whether uploaded files with an
|
||||
audio `Content-Type` are actually playable.
|
||||
|
||||
**Changes**: New in Zulip 11.0 (feature level 405).
|
||||
|
||||
## Mentions and silent mentions
|
||||
|
||||
Zulip markup supports [mentioning](/help/mention-a-user-or-group)
|
||||
@@ -402,33 +345,7 @@ features.
|
||||
|
||||
## Removed features
|
||||
|
||||
### Removed legacy Dropbox link preview markup
|
||||
|
||||
In Zulip 11.0 (feature level 395), the Zulip server stopped generating
|
||||
legacy Dropbox link previews. Dropbox links are now previewed just
|
||||
like standard Zulip image/link previews. However, some legacy Dropbox
|
||||
previews may exist in existing messages.
|
||||
|
||||
Clients are recommended to prune these previews from message HTML;
|
||||
since they always appear after the actual link, there is no loss of
|
||||
information/functionality. They can be recognized via the classes
|
||||
`message_inline_ref`, `message_inline_image_desc`, and
|
||||
`message_inline_image_title`:
|
||||
|
||||
``` html
|
||||
<div class="message_inline_ref">
|
||||
<a href="https://www.dropbox.com/sh/cm39k9e04z7fhim/AAAII5NK-9daee3FcF41anEua?dl=" title="Saves">
|
||||
<img src="/path/to/folder_dropbox.png">
|
||||
</a>
|
||||
<div><div class="message_inline_image_title">Saves</div>
|
||||
<desc class="message_inline_image_desc"></desc>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Removed legacy avatar markup
|
||||
|
||||
In Zulip 4.0 (feature level 24), the rarely used `!avatar()`
|
||||
**Changes**: In Zulip 4.0 (feature level 24), the rarely used `!avatar()`
|
||||
and `!gravatar()` markup syntax, which was never documented and had an
|
||||
inconsistent syntax, were removed.
|
||||
|
||||
|
@@ -1,130 +0,0 @@
|
||||
# Mobile notifications
|
||||
|
||||
Zulip Server 11.0+ supports end-to-end encryption (E2EE) for mobile
|
||||
push notifications. Mobile push notifications sent by all Zulip
|
||||
servers go through Zulip's mobile push notifications service, which
|
||||
then delivers the notifications through the appropriate
|
||||
platform-specific push notification service (Google's FCM or Apple's
|
||||
APNs). E2EE push notifications ensure that mobile notification message
|
||||
content and metadata is not visible to intermediaries.
|
||||
|
||||
Mobile clients that have [registered an E2EE push
|
||||
device](/api/register-push-device) will receive mobile notifications
|
||||
end-to-end encrypted by their Zulip server.
|
||||
|
||||
This page documents the format of the encrypted JSON-format payloads
|
||||
that the client will receive through this protocol. The same encrypted
|
||||
payload formats are used for both Firebase Cloud Messaging (FCM) and
|
||||
Apple Push Notification service (APNs).
|
||||
|
||||
## Payload examples
|
||||
|
||||
### New channel message
|
||||
|
||||
Sample JSON data that gets encrypted:
|
||||
```json
|
||||
{
|
||||
"channel_id": 10,
|
||||
"channel_name": "Denmark",
|
||||
"content": "@test_user_group",
|
||||
"mentioned_user_group_id": 41,
|
||||
"mentioned_user_group_name": "test_user_group",
|
||||
"message_id": 45,
|
||||
"realm_name": "Zulip Dev",
|
||||
"realm_url": "http://zulip.testserver",
|
||||
"recipient_type": "channel",
|
||||
"sender_avatar_url": "https://secure.gravatar.com/avatar/818c212b9f8830dfef491b3f7da99a14?d=identicon&version=1",
|
||||
"sender_full_name": "aaron",
|
||||
"sender_id": 6,
|
||||
"time": 1754385395,
|
||||
"topic": "test",
|
||||
"type": "message",
|
||||
"user_id": 10
|
||||
}
|
||||
```
|
||||
|
||||
- The `mentioned_user_group_id` and `mentioned_user_group_name` fields
|
||||
are only present for messages that mention a group containing the
|
||||
current user, and triggered a mobile notification because of that
|
||||
group mention. For example, messages that mention both the user
|
||||
directly and a group containing the user, these fields will not be
|
||||
present in the payload, because the direct mention has precedence.
|
||||
|
||||
**Changes**: New in Zulip 11.0 (feature level 413).
|
||||
|
||||
### New direct message
|
||||
|
||||
Sample JSON data that gets encrypted:
|
||||
```json
|
||||
{
|
||||
"content": "test content",
|
||||
"message_id": 46,
|
||||
"pm_users": "6,10,12,15",
|
||||
"realm_name": "Zulip Dev",
|
||||
"realm_url": "http://zulip.testserver",
|
||||
"recipient_type": "direct",
|
||||
"sender_avatar_url": "https://secure.gravatar.com/avatar/818c212b9f8830dfef491b3f7da99a14?d=identicon&version=1",
|
||||
"sender_full_name": "aaron",
|
||||
"sender_id": 6,
|
||||
"time": 1754385290,
|
||||
"type": "message",
|
||||
"user_id": 10
|
||||
}
|
||||
```
|
||||
|
||||
- **Group direct messages**: The `pm_users` string field is only
|
||||
present for group direct messages, containing a sorted comma-separated
|
||||
list of all user IDs in the group direct message conversation,
|
||||
including both `user_id` and `sender_id`.
|
||||
|
||||
**Changes**: New in Zulip 11.0 (feature level 413).
|
||||
|
||||
### New group direct message
|
||||
|
||||
### Remove notifications
|
||||
|
||||
When a batch of messages that had previously been included in mobile
|
||||
notifications are marked as read, are deleted, become inaccessible, or
|
||||
otherwise should no longer be displayed to the user, a removal
|
||||
notification is sent.
|
||||
|
||||
Sample JSON data that gets encrypted:
|
||||
```json
|
||||
{
|
||||
"message_ids": [
|
||||
31,
|
||||
32
|
||||
],
|
||||
"realm_name": "Zulip Dev",
|
||||
"realm_url": "http://zulip.testserver",
|
||||
"type": "remove",
|
||||
"user_id": 10
|
||||
}
|
||||
```
|
||||
|
||||
[zulip-bouncer]: https://zulip.readthedocs.io/en/latest/production/mobile-push-notifications.html#mobile-push-notification-service
|
||||
|
||||
**Changes**: New in Zulip 11.0 (feature level 413).
|
||||
|
||||
### Test push notification
|
||||
|
||||
A user can trigger [sending an E2EE test push notification](/api/e2ee-test-notify)
|
||||
to the user's selected mobile device or all of their mobile devices.
|
||||
|
||||
Sample JSON data that gets encrypted:
|
||||
```json
|
||||
{
|
||||
"realm_name": "Zulip Dev",
|
||||
"realm_url": "http://zulip.testserver",
|
||||
"time": 1754577820,
|
||||
"type": "test",
|
||||
"user_id": 10
|
||||
}
|
||||
```
|
||||
|
||||
**Changes**: New in Zulip 11.0 (feature level 420).
|
||||
|
||||
## Future work
|
||||
|
||||
This page will eventually also document the formats of the APNs and
|
||||
FCM payloads wrapping the encrypted content.
|
@@ -93,13 +93,13 @@ Note that an outgoing webhook bot can use the [Zulip REST
|
||||
API](/api/rest) with its API key in case your bot needs to do
|
||||
something else, like add an emoji reaction or upload a file.
|
||||
|
||||
## Slack-compatible webhook format
|
||||
## Slack-format webhook format
|
||||
|
||||
The Slack-compatible webhook format allows immediate integration with
|
||||
many third-party systems that already support Slack outgoing webhooks.
|
||||
|
||||
The following table details how the Zulip server translates a Zulip
|
||||
message into the Slack-compatible webhook format.
|
||||
This interface translates Zulip's outgoing webhook's request into the
|
||||
format that Slack's outgoing webhook interface sends. As a result,
|
||||
one should be able to use this to interact with third-party
|
||||
integrations designed to work with Slack's outgoing webhook interface.
|
||||
Here's how we fill in the fields that a Slack-format webhook expects:
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
|
@@ -36,8 +36,7 @@ by the [events API](/api/get-events).
|
||||
|
||||
Note that [`POST /register`](/api/register-queue) also returns an
|
||||
`is_moderator` boolean property specifying whether the current user is
|
||||
at least an organization moderator. The property will be true for admins
|
||||
and owners too.
|
||||
an organization moderator.
|
||||
|
||||
Additionally, user account data include an `is_billing_admin` property
|
||||
specifying whether the user is a billing administrator for the Zulip
|
||||
|
@@ -13,7 +13,7 @@ You'll need:
|
||||
|
||||
* An account in a Zulip organization
|
||||
(e.g., [the Zulip development community](https://zulip.com/development-community/),
|
||||
`{{ display_host }}`, or a Zulip organization on your own
|
||||
`<yourSubdomain>.zulipchat.com`, or a Zulip organization on your own
|
||||
[development](https://zulip.readthedocs.io/en/latest/development/overview.html) or
|
||||
[production](https://zulip.readthedocs.io/en/latest/production/install.html) server).
|
||||
* A computer where you're running the bot from.
|
||||
|
3
api_docs/unmerged.d/ZF-5769ba
Normal file
3
api_docs/unmerged.d/ZF-5769ba
Normal file
@@ -0,0 +1,3 @@
|
||||
* [`POST /register`](/api/register-queue): The `streams` data
|
||||
structure now includes archived channels for clients that
|
||||
declared the `archived_channels` client capability.
|
5
api_docs/unmerged.d/ZF-960986
Normal file
5
api_docs/unmerged.d/ZF-960986
Normal file
@@ -0,0 +1,5 @@
|
||||
* [`GET /events`](/api/get-events): Archiving and unarchiving
|
||||
streams now send `update` events to clients that declared
|
||||
the `archived_channels` client capability. `delete` and `create`
|
||||
events are still sent to clients that did not declare
|
||||
`archived_channels` client capability.
|
@@ -1,112 +0,0 @@
|
||||
# Generated by Django 5.2.3 on 2025-07-04 20:33
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, transaction
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.migrations.state import StateApps
|
||||
from django.db.models import Max
|
||||
|
||||
|
||||
def migrate_realmcreationkey_to_realmcreationstatus(
|
||||
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
|
||||
) -> None:
|
||||
"""
|
||||
This migration is for switching from using a separate RealmCreationKey class
|
||||
for realm creation keys to just using the Confirmation system.
|
||||
|
||||
The aim is to iterate through all the existing RealmCreationKey and create
|
||||
a corresponding Confirmation+RealmCreationStatus.
|
||||
|
||||
For validity of these objects, we only need to worry about RealmCreationKeys
|
||||
expired due to time. This is taken care of by making sure we set expiry_date
|
||||
on the Confirmation we're creating.
|
||||
The way the RealmCreationKey system worked was to .delete() the RealmCreationKey
|
||||
objects when they were used - so those simply no longer exist and we don't need to
|
||||
worry about this case.
|
||||
"""
|
||||
CAN_CREATE_REALM = 11
|
||||
|
||||
Confirmation = apps.get_model("confirmation", "Confirmation")
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
RealmCreationKey = apps.get_model("confirmation", "RealmCreationKey")
|
||||
RealmCreationStatus = apps.get_model("zerver", "RealmCreationStatus")
|
||||
realm_creation_status_content_type, created = ContentType.objects.get_or_create(
|
||||
model="realmcreationstatus", app_label="zerver"
|
||||
)
|
||||
|
||||
BATCH_SIZE = 10000
|
||||
max_id = RealmCreationKey.objects.aggregate(Max("id"))["id__max"]
|
||||
if max_id is None:
|
||||
# Nothing to do.
|
||||
return
|
||||
|
||||
lower_bound = 1
|
||||
while lower_bound <= max_id + (BATCH_SIZE / 2):
|
||||
upper_bound = lower_bound + BATCH_SIZE - 1
|
||||
creation_keys = RealmCreationKey.objects.filter(id__range=(lower_bound, upper_bound))
|
||||
creation_statuses_to_create = []
|
||||
confirmations_to_create = []
|
||||
for creation_key in creation_keys:
|
||||
# These keys were generated in the same way as keys for Confirmation objects,
|
||||
# so we can copy them over without breaking anything.
|
||||
key = creation_key.creation_key
|
||||
date_created = creation_key.date_created
|
||||
presume_email_valid = creation_key.presume_email_valid
|
||||
|
||||
creation_status = RealmCreationStatus(
|
||||
status=0, date_created=date_created, presume_email_valid=presume_email_valid
|
||||
)
|
||||
confirmation = Confirmation(
|
||||
content_type=realm_creation_status_content_type,
|
||||
type=CAN_CREATE_REALM,
|
||||
confirmation_key=key,
|
||||
date_sent=date_created,
|
||||
expiry_date=date_created
|
||||
+ timedelta(days=settings.CAN_CREATE_REALM_LINK_VALIDITY_DAYS),
|
||||
)
|
||||
|
||||
# To attach the Confirmations to RealmCreationStatus objects we need to set the
|
||||
# confirmation.object_id to their respective ids. But we haven't saved
|
||||
# the RealmCreationStatus objs to the database yet - so we don't have ids.
|
||||
#
|
||||
# After we .bulk_create() them, their .id attributes will be populated.
|
||||
# So for now we just link the RealmCreationStatus to the Confirmation
|
||||
# via a temporary ._object attribute - which we'll use later to set
|
||||
# the .object_id as intended.
|
||||
confirmation._object = creation_status
|
||||
|
||||
creation_statuses_to_create.append(creation_status)
|
||||
confirmations_to_create.append(confirmation)
|
||||
with transaction.atomic():
|
||||
RealmCreationStatus.objects.bulk_create(creation_statuses_to_create)
|
||||
|
||||
# Now the objects in creation_statuses_to_create have had their .id
|
||||
# attrs populated. For every confirmation, confirmation._object
|
||||
# points to its corresponding RealmCreationStatus - so now we can
|
||||
# set the confirmation.object_id values and clear out the temporary
|
||||
# ._object attr.
|
||||
for confirmation in confirmations_to_create:
|
||||
confirmation.object_id = confirmation._object.id
|
||||
delattr(confirmation, "_object")
|
||||
Confirmation.objects.bulk_create(confirmations_to_create)
|
||||
|
||||
lower_bound += BATCH_SIZE
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("confirmation", "0015_alter_confirmation_object_id"),
|
||||
("zerver", "0747_realmcreationstatus"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
migrate_realmcreationkey_to_realmcreationstatus,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
elidable=True,
|
||||
),
|
||||
]
|
@@ -1,15 +0,0 @@
|
||||
# Generated by Django 5.2.3 on 2025-07-04 19:08
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("confirmation", "0016_realmcreationkey_to_realmcreationstatus"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="RealmCreationKey",
|
||||
),
|
||||
]
|
@@ -5,7 +5,7 @@ import secrets
|
||||
from base64 import b32encode
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
from typing import TypeAlias, Union, cast
|
||||
from typing import Optional, TypeAlias, Union, cast
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
@@ -30,7 +30,6 @@ from zerver.models import (
|
||||
RealmReactivationStatus,
|
||||
UserProfile,
|
||||
)
|
||||
from zerver.models.prereg_users import RealmCreationStatus
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
from zilencer.models import (
|
||||
@@ -71,7 +70,6 @@ NoZilencerConfirmationObjT: TypeAlias = (
|
||||
| EmailChangeStatus
|
||||
| UserProfile
|
||||
| RealmReactivationStatus
|
||||
| RealmCreationStatus
|
||||
)
|
||||
ZilencerConfirmationObjT: TypeAlias = Union[
|
||||
NoZilencerConfirmationObjT,
|
||||
@@ -155,7 +153,7 @@ def create_confirmation_object(
|
||||
realm = None
|
||||
else:
|
||||
obj = cast(NoZilencerConfirmationObjT, obj)
|
||||
assert not isinstance(obj, PreregistrationRealm | RealmCreationStatus)
|
||||
assert not isinstance(obj, PreregistrationRealm)
|
||||
realm = obj.realm
|
||||
|
||||
current_time = timezone_now()
|
||||
@@ -187,17 +185,15 @@ def create_confirmation_link(
|
||||
url_args: Mapping[str, str] = {},
|
||||
no_associated_realm_object: bool = False,
|
||||
) -> str:
|
||||
conf = create_confirmation_object(
|
||||
obj,
|
||||
confirmation_type,
|
||||
validity_in_minutes=validity_in_minutes,
|
||||
no_associated_realm_object=no_associated_realm_object,
|
||||
)
|
||||
result = confirmation_url_for(
|
||||
conf,
|
||||
return confirmation_url_for(
|
||||
create_confirmation_object(
|
||||
obj,
|
||||
confirmation_type,
|
||||
validity_in_minutes=validity_in_minutes,
|
||||
no_associated_realm_object=no_associated_realm_object,
|
||||
),
|
||||
url_args=url_args,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def confirmation_url_for(confirmation_obj: "Confirmation", url_args: Mapping[str, str] = {}) -> str:
|
||||
@@ -236,11 +232,10 @@ class Confirmation(models.Model):
|
||||
UNSUBSCRIBE = 4
|
||||
SERVER_REGISTRATION = 5
|
||||
MULTIUSE_INVITE = 6
|
||||
NEW_REALM_USER_REGISTRATION = 7
|
||||
REALM_CREATION = 7
|
||||
REALM_REACTIVATION = 8
|
||||
REMOTE_SERVER_BILLING_LEGACY_LOGIN = 9
|
||||
REMOTE_REALM_BILLING_LEGACY_LOGIN = 10
|
||||
CAN_CREATE_REALM = 11
|
||||
type = models.PositiveSmallIntegerField()
|
||||
|
||||
class Meta:
|
||||
@@ -269,7 +264,7 @@ _properties = {
|
||||
Confirmation.INVITATION: ConfirmationType(
|
||||
"get_prereg_key_and_redirect", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
|
||||
),
|
||||
Confirmation.EMAIL_CHANGE: ConfirmationType("confirm_email_change_get"),
|
||||
Confirmation.EMAIL_CHANGE: ConfirmationType("confirm_email_change"),
|
||||
Confirmation.UNSUBSCRIBE: ConfirmationType(
|
||||
"unsubscribe",
|
||||
validity_in_days=1000000, # should never expire
|
||||
@@ -277,11 +272,8 @@ _properties = {
|
||||
Confirmation.MULTIUSE_INVITE: ConfirmationType(
|
||||
"join", validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS
|
||||
),
|
||||
Confirmation.CAN_CREATE_REALM: ConfirmationType(
|
||||
"create_realm", validity_in_days=settings.CAN_CREATE_REALM_LINK_VALIDITY_DAYS
|
||||
),
|
||||
Confirmation.NEW_REALM_USER_REGISTRATION: ConfirmationType("get_prereg_key_and_redirect"),
|
||||
Confirmation.REALM_REACTIVATION: ConfirmationType("realm_reactivation_get"),
|
||||
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(
|
||||
@@ -302,7 +294,47 @@ def one_click_unsubscribe_link(user_profile: UserProfile, email_type: str) -> st
|
||||
)
|
||||
|
||||
|
||||
def generate_realm_creation_url(by_admin: bool = False) -> str:
|
||||
from zerver.views.registration import prepare_realm_creation_url
|
||||
# Functions related to links generated by the generate_realm_creation_link.py
|
||||
# management command.
|
||||
# Note that being validated here will just allow the user to access the create_realm
|
||||
# form, where they will enter their email and go through the regular
|
||||
# Confirmation.REALM_CREATION pathway.
|
||||
# Arguably RealmCreationKey should just be another ConfirmationObjT and we should
|
||||
# add another Confirmation.type for this; it's this way for historical reasons.
|
||||
|
||||
return prepare_realm_creation_url(presume_email_valid=by_admin)
|
||||
|
||||
def validate_key(creation_key: str | None) -> Optional["RealmCreationKey"]:
|
||||
"""Get the record for this key, raising InvalidCreationKey if non-None but invalid."""
|
||||
if creation_key is None:
|
||||
return None
|
||||
try:
|
||||
key_record = RealmCreationKey.objects.get(creation_key=creation_key)
|
||||
except RealmCreationKey.DoesNotExist:
|
||||
raise RealmCreationKey.InvalidError
|
||||
time_elapsed = timezone_now() - key_record.date_created
|
||||
if time_elapsed.total_seconds() > settings.REALM_CREATION_LINK_VALIDITY_DAYS * 24 * 3600:
|
||||
raise RealmCreationKey.InvalidError
|
||||
return key_record
|
||||
|
||||
|
||||
def generate_realm_creation_url(by_admin: bool = False) -> str:
|
||||
key = generate_key()
|
||||
RealmCreationKey.objects.create(
|
||||
creation_key=key, date_created=timezone_now(), presume_email_valid=by_admin
|
||||
)
|
||||
return urljoin(
|
||||
settings.ROOT_DOMAIN_URI,
|
||||
reverse("create_realm", kwargs={"creation_key": key}),
|
||||
)
|
||||
|
||||
|
||||
class RealmCreationKey(models.Model):
|
||||
creation_key = models.CharField("activation key", db_index=True, max_length=40)
|
||||
date_created = models.DateTimeField("created", default=timezone_now)
|
||||
|
||||
# True just if we should presume the email address the user enters
|
||||
# is theirs, and skip sending mail to it to confirm that.
|
||||
presume_email_valid = models.BooleanField(default=False)
|
||||
|
||||
class InvalidError(Exception):
|
||||
pass
|
||||
|
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
@@ -15,9 +16,9 @@ from django.utils.timezone import now as timezone_now
|
||||
from markupsafe import Markup
|
||||
from psycopg2.sql import Composable
|
||||
|
||||
from corporate.models.licenses import LicenseLedger
|
||||
from corporate.models.plans import CustomerPlan
|
||||
from corporate.models import CustomerPlan, LicenseLedger
|
||||
from zerver.lib.pysa import mark_sanitized
|
||||
from zerver.lib.url_encoding import append_url_query_string
|
||||
from zerver.models import Realm
|
||||
from zilencer.models import (
|
||||
RemoteCustomerUserCount,
|
||||
@@ -137,12 +138,16 @@ def realm_stats_link(realm_str: str) -> Markup:
|
||||
|
||||
|
||||
def user_support_link(email: str) -> Markup:
|
||||
url = reverse("support", query={"q": email})
|
||||
support_url = reverse("support")
|
||||
query = urlencode({"q": email})
|
||||
url = append_url_query_string(support_url, query)
|
||||
return Markup('<a href="{url}"><i class="fa fa-gear"></i></a>').format(url=url)
|
||||
|
||||
|
||||
def realm_support_link(realm_str: str) -> Markup:
|
||||
url = reverse("support", query={"q": realm_str})
|
||||
support_url = reverse("support")
|
||||
query = urlencode({"q": realm_str})
|
||||
url = append_url_query_string(support_url, query)
|
||||
return Markup('<a href="{url}">{realm}</i></a>').format(url=url, realm=realm_str)
|
||||
|
||||
|
||||
@@ -160,17 +165,13 @@ def remote_installation_stats_link(server_id: int) -> Markup:
|
||||
|
||||
|
||||
def remote_installation_support_link(hostname: str) -> Markup:
|
||||
url = reverse("remote_servers_support", query={"q": hostname})
|
||||
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)
|
||||
|
||||
|
||||
def get_plan_rate_percentage(discount: str | None, has_fixed_price: bool) -> str:
|
||||
# We want to clearly note plans with a fixed price, and not show
|
||||
# them as paying 100%, as they are usually a special, negotiated
|
||||
# rate with the customer.
|
||||
if has_fixed_price:
|
||||
return "Fixed"
|
||||
|
||||
def get_plan_rate_percentage(discount: str | None) -> str:
|
||||
# CustomerPlan.discount is a string field that stores the discount.
|
||||
if discount is None or discount == "0":
|
||||
return "100%"
|
||||
@@ -192,7 +193,6 @@ def get_remote_activity_plan_data(
|
||||
) -> RemoteActivityPlanData:
|
||||
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
|
||||
|
||||
has_fixed_price = plan.fixed_price is not None
|
||||
if plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY or plan.status in (
|
||||
CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL,
|
||||
CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE,
|
||||
@@ -206,13 +206,13 @@ def get_remote_activity_plan_data(
|
||||
renewal_cents = RemoteRealmBillingSession(
|
||||
remote_realm=remote_realm
|
||||
).get_annual_recurring_revenue_for_support_data(plan, license_ledger)
|
||||
current_rate = get_plan_rate_percentage(plan.discount, has_fixed_price)
|
||||
current_rate = get_plan_rate_percentage(plan.discount)
|
||||
else:
|
||||
assert remote_server is not None
|
||||
renewal_cents = RemoteServerBillingSession(
|
||||
remote_server=remote_server
|
||||
).get_annual_recurring_revenue_for_support_data(plan, license_ledger)
|
||||
current_rate = get_plan_rate_percentage(plan.discount, has_fixed_price)
|
||||
current_rate = get_plan_rate_percentage(plan.discount)
|
||||
|
||||
return RemoteActivityPlanData(
|
||||
current_status=plan.get_plan_status_as_text(),
|
||||
@@ -253,10 +253,7 @@ def get_estimated_arr_and_rate_by_realm() -> tuple[dict[str, int], dict[str, str
|
||||
realm=plan.customer.realm
|
||||
).get_annual_recurring_revenue_for_support_data(plan, latest_ledger_entry)
|
||||
annual_revenue[plan.customer.realm.string_id] = renewal_cents
|
||||
has_fixed_price = plan.fixed_price is not None
|
||||
plan_rate[plan.customer.realm.string_id] = get_plan_rate_percentage(
|
||||
plan.discount, has_fixed_price
|
||||
)
|
||||
plan_rate[plan.customer.realm.string_id] = get_plan_rate_percentage(plan.discount)
|
||||
return annual_revenue, plan_rate
|
||||
|
||||
|
||||
|
@@ -123,9 +123,7 @@ def authenticated_remote_realm_management_endpoint(
|
||||
url = append_url_query_string(url, query)
|
||||
|
||||
# Return error for AJAX requests with url.
|
||||
if (
|
||||
request.get_preferred_type(["application/json", "text/html"]) != "text/html"
|
||||
): # nocoverage
|
||||
if request.headers.get("x-requested-with") == "XMLHttpRequest": # nocoverage
|
||||
return session_expired_ajax_response(url)
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -202,16 +200,14 @@ def authenticated_remote_server_management_endpoint(
|
||||
# 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)
|
||||
url = reverse(
|
||||
"remote_billing_legacy_server_login",
|
||||
query=None if page_type is None else {"next_page": page_type},
|
||||
)
|
||||
if page_type is not None:
|
||||
query = urlencode({"next_page": page_type})
|
||||
url = append_url_query_string(url, query)
|
||||
|
||||
# Return error for AJAX requests with url.
|
||||
if (
|
||||
request.get_preferred_type(["application/json", "text/html"]) != "text/html"
|
||||
): # nocoverage
|
||||
if request.headers.get("x-requested-with") == "XMLHttpRequest": # nocoverage
|
||||
return session_expired_ajax_response(url)
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
|
@@ -2,7 +2,7 @@ from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from corporate.lib.stripe import LicenseLimitError, get_latest_seat_count, get_seat_count
|
||||
from corporate.models.plans import CustomerPlan, get_current_plan_by_realm
|
||||
from corporate.models import CustomerPlan, get_current_plan_by_realm
|
||||
from zerver.actions.create_user import send_group_direct_message_to_admins
|
||||
from zerver.lib.exceptions import InvitationError, JsonableError
|
||||
from zerver.models import Realm, UserProfile
|
||||
|
@@ -28,21 +28,21 @@ from django.utils.translation import override as override_language
|
||||
from typing_extensions import ParamSpec, override
|
||||
|
||||
from corporate.lib.billing_types import BillingModality, BillingSchedule, LicenseManagement
|
||||
from corporate.models.customers import (
|
||||
from corporate.models import (
|
||||
Customer,
|
||||
CustomerPlan,
|
||||
CustomerPlanOffer,
|
||||
Invoice,
|
||||
LicenseLedger,
|
||||
Session,
|
||||
SponsoredPlanTypes,
|
||||
ZulipSponsorshipRequest,
|
||||
get_current_plan_by_customer,
|
||||
get_current_plan_by_realm,
|
||||
get_customer_by_realm,
|
||||
get_customer_by_remote_realm,
|
||||
get_customer_by_remote_server,
|
||||
)
|
||||
from corporate.models.licenses import LicenseLedger
|
||||
from corporate.models.plans import (
|
||||
CustomerPlan,
|
||||
CustomerPlanOffer,
|
||||
get_current_plan_by_customer,
|
||||
get_current_plan_by_realm,
|
||||
)
|
||||
from corporate.models.sponsorships import SponsoredPlanTypes, ZulipSponsorshipRequest
|
||||
from corporate.models.stripe_state import Invoice, Session
|
||||
from zerver.lib.cache import cache_with_key, get_realm_seat_count_cache_key
|
||||
from zerver.lib.exceptions import JsonableError
|
||||
from zerver.lib.logging_util import log_to_file
|
||||
@@ -52,6 +52,7 @@ from zerver.lib.send_email import (
|
||||
send_email_to_users_with_billing_access_and_realm_owners,
|
||||
)
|
||||
from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime
|
||||
from zerver.lib.url_encoding import append_url_query_string
|
||||
from zerver.lib.utils import assert_is_not_none
|
||||
from zerver.models import Realm, RealmAuditLog, Stream, UserProfile
|
||||
from zerver.models.realm_audit_logs import AuditLogEventType
|
||||
@@ -102,7 +103,7 @@ CARD_CAPITALIZATION = {
|
||||
}
|
||||
|
||||
# The version of Stripe API the billing system supports.
|
||||
STRIPE_API_VERSION = "2025-04-30.basil"
|
||||
STRIPE_API_VERSION = "2020-08-27"
|
||||
|
||||
stripe.api_version = STRIPE_API_VERSION
|
||||
|
||||
@@ -377,7 +378,10 @@ def payment_method_string(stripe_customer: stripe.Customer) -> str:
|
||||
|
||||
def build_support_url(support_view: str, query_text: str) -> str:
|
||||
support_realm_url = get_realm(settings.STAFF_SUBDOMAIN).url
|
||||
return urljoin(support_realm_url, reverse(support_view, query={"q": query_text}))
|
||||
support_url = urljoin(support_realm_url, reverse(support_view))
|
||||
query = urlencode({"q": query_text})
|
||||
support_url = append_url_query_string(support_url, query)
|
||||
return support_url
|
||||
|
||||
|
||||
def get_configured_fixed_price_plan_offer(
|
||||
@@ -517,7 +521,7 @@ def sponsorship_org_type_key_helper(d: Any) -> int:
|
||||
|
||||
class PriceArgs(TypedDict, total=False):
|
||||
amount: int
|
||||
unit_amount_decimal: str
|
||||
unit_amount: int
|
||||
quantity: int
|
||||
|
||||
|
||||
@@ -623,6 +627,7 @@ class BillingSessionAuditLogEventError(Exception):
|
||||
class UpgradePageParams(TypedDict):
|
||||
page_type: Literal["upgrade"]
|
||||
annual_price: int
|
||||
demo_organization_scheduled_deletion_date: datetime | None
|
||||
monthly_price: int
|
||||
seat_count: int
|
||||
billing_base_url: str
|
||||
@@ -639,6 +644,8 @@ class UpgradePageParams(TypedDict):
|
||||
class UpgradePageSessionTypeSpecificContext(TypedDict):
|
||||
customer_name: str
|
||||
email: str
|
||||
is_demo_organization: bool
|
||||
demo_organization_scheduled_deletion_date: datetime | None
|
||||
is_self_hosting: bool
|
||||
|
||||
|
||||
@@ -661,6 +668,7 @@ class UpgradePageContext(TypedDict):
|
||||
stripe_email: str
|
||||
exempt_from_license_number_check: bool
|
||||
free_trial_end_date: str | None
|
||||
is_demo_organization: bool
|
||||
manual_license_management: bool
|
||||
using_min_licenses_for_plan: bool
|
||||
min_licenses_for_plan: int
|
||||
@@ -863,6 +871,42 @@ class BillingSession(ABC):
|
||||
assert customer.stripe_customer_id is not None
|
||||
plan_name = CustomerPlan.name_from_tier(plan_tier)
|
||||
assert price_per_license is None or fixed_price is None
|
||||
price_args: PriceArgs = {}
|
||||
if fixed_price is None:
|
||||
assert price_per_license is not None
|
||||
price_args = {
|
||||
"quantity": licenses,
|
||||
"unit_amount": price_per_license,
|
||||
}
|
||||
else:
|
||||
assert fixed_price is not None
|
||||
amount_due = get_amount_due_fixed_price_plan(fixed_price, billing_schedule)
|
||||
price_args = {"amount": amount_due}
|
||||
|
||||
stripe.InvoiceItem.create(
|
||||
currency="usd",
|
||||
customer=customer.stripe_customer_id,
|
||||
description=plan_name,
|
||||
discountable=False,
|
||||
period=invoice_period,
|
||||
**price_args,
|
||||
)
|
||||
|
||||
if fixed_price is None and customer.flat_discounted_months > 0:
|
||||
num_months = 12 if billing_schedule == CustomerPlan.BILLING_SCHEDULE_ANNUAL else 1
|
||||
flat_discounted_months = min(customer.flat_discounted_months, num_months)
|
||||
discount = customer.flat_discount * flat_discounted_months
|
||||
customer.flat_discounted_months -= flat_discounted_months
|
||||
customer.save(update_fields=["flat_discounted_months"])
|
||||
|
||||
stripe.InvoiceItem.create(
|
||||
currency="usd",
|
||||
customer=customer.stripe_customer_id,
|
||||
description=f"${cents_to_dollar_string(customer.flat_discount)}/month new customer discount",
|
||||
# Negative value to apply discount.
|
||||
amount=(-1 * discount),
|
||||
period=invoice_period,
|
||||
)
|
||||
|
||||
if charge_automatically:
|
||||
collection_method: Literal["charge_automatically", "send_invoice"] = (
|
||||
@@ -902,46 +946,6 @@ class BillingSession(ABC):
|
||||
if days_until_due is not None:
|
||||
invoice_params["days_until_due"] = days_until_due
|
||||
stripe_invoice = stripe.Invoice.create(**invoice_params)
|
||||
assert stripe_invoice.id is not None
|
||||
|
||||
price_args: PriceArgs = {}
|
||||
if fixed_price is None:
|
||||
assert price_per_license is not None
|
||||
price_args = {
|
||||
"quantity": licenses,
|
||||
"unit_amount_decimal": str(price_per_license),
|
||||
}
|
||||
else:
|
||||
assert fixed_price is not None
|
||||
amount_due = get_amount_due_fixed_price_plan(fixed_price, billing_schedule)
|
||||
price_args = {"amount": amount_due}
|
||||
|
||||
stripe.InvoiceItem.create(
|
||||
invoice=stripe_invoice.id,
|
||||
currency="usd",
|
||||
customer=customer.stripe_customer_id,
|
||||
description=plan_name,
|
||||
discountable=False,
|
||||
period=invoice_period,
|
||||
**price_args,
|
||||
)
|
||||
|
||||
if fixed_price is None and customer.flat_discounted_months > 0:
|
||||
num_months = 12 if billing_schedule == CustomerPlan.BILLING_SCHEDULE_ANNUAL else 1
|
||||
flat_discounted_months = min(customer.flat_discounted_months, num_months)
|
||||
discount = customer.flat_discount * flat_discounted_months
|
||||
customer.flat_discounted_months -= flat_discounted_months
|
||||
customer.save(update_fields=["flat_discounted_months"])
|
||||
|
||||
stripe.InvoiceItem.create(
|
||||
invoice=stripe_invoice.id,
|
||||
currency="usd",
|
||||
customer=customer.stripe_customer_id,
|
||||
description=f"${cents_to_dollar_string(customer.flat_discount)}/month new customer discount",
|
||||
# Negative value to apply discount.
|
||||
amount=(-1 * discount),
|
||||
period=invoice_period,
|
||||
)
|
||||
stripe.Invoice.finalize_invoice(stripe_invoice)
|
||||
return stripe_invoice
|
||||
|
||||
@@ -1189,8 +1193,8 @@ class BillingSession(ABC):
|
||||
is_created_for_free_trial_upgrade=current_plan_id is not None and on_free_trial,
|
||||
)
|
||||
|
||||
if stripe_invoice.status != "paid" and charge_automatically:
|
||||
# Stripe can take its sweet hour to charge customers after creating an invoice.
|
||||
if charge_automatically:
|
||||
# Stripe takes its sweet hour to charge customers after creating an invoice.
|
||||
# Since we want to charge customers immediately, we charge them manually.
|
||||
# Then poll for the status of the invoice to see if the payment succeeded.
|
||||
stripe_invoice = stripe.Invoice.pay(stripe_invoice.id)
|
||||
@@ -2301,24 +2305,6 @@ class BillingSession(ABC):
|
||||
# This will create invoice for any additional licenses that user has at the time of
|
||||
# switching from free trial to paid plan since they already paid for the plan's this billing cycle.
|
||||
is_renewal = False
|
||||
|
||||
# Since we need to move the `billing_cycle_anchor` forward below, we also
|
||||
# need to update the `event_time` of the last renewal ledger entry to avoid
|
||||
# our logic from thinking that licenses for the current billing cycle hasn't
|
||||
# been paid for.
|
||||
last_renewal_ledger_entry = (
|
||||
LicenseLedger.objects.filter(
|
||||
plan=plan,
|
||||
is_renewal=True,
|
||||
)
|
||||
.order_by("-id")
|
||||
.first()
|
||||
)
|
||||
assert last_renewal_ledger_entry is not None
|
||||
last_renewal_ledger_entry.event_time = next_billing_cycle.replace(
|
||||
microsecond=0
|
||||
)
|
||||
last_renewal_ledger_entry.save(update_fields=["event_time"])
|
||||
else:
|
||||
# We end the free trial since customer hasn't paid.
|
||||
plan.status = CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL
|
||||
@@ -2839,11 +2825,15 @@ class BillingSession(ABC):
|
||||
"stripe_email": stripe_email,
|
||||
"exempt_from_license_number_check": exempt_from_license_number_check,
|
||||
"free_trial_end_date": free_trial_end_date,
|
||||
"is_demo_organization": customer_specific_context["is_demo_organization"],
|
||||
"complimentary_access_plan_end_date": complimentary_access_plan_end_date,
|
||||
"manual_license_management": initial_upgrade_request.manual_license_management,
|
||||
"page_params": {
|
||||
"page_type": "upgrade",
|
||||
"annual_price": annual_price,
|
||||
"demo_organization_scheduled_deletion_date": customer_specific_context[
|
||||
"demo_organization_scheduled_deletion_date"
|
||||
],
|
||||
"monthly_price": monthly_price,
|
||||
"seat_count": seat_count,
|
||||
"billing_base_url": self.billing_base_url,
|
||||
@@ -3046,7 +3036,7 @@ class BillingSession(ABC):
|
||||
|
||||
licenses = update_plan_request.licenses
|
||||
if licenses is not None:
|
||||
if plan.is_free_trial():
|
||||
if plan.is_free_trial(): # nocoverage
|
||||
raise JsonableError(
|
||||
_("Cannot update licenses in the current billing period for free trial plan.")
|
||||
)
|
||||
@@ -3230,36 +3220,25 @@ class BillingSession(ABC):
|
||||
licenses_base = plan.invoiced_through.licenses
|
||||
invoiced_through_id = plan.invoiced_through.id
|
||||
|
||||
# Mark the plan for start of invoicing process.
|
||||
plan.invoicing_status = CustomerPlan.INVOICING_STATUS_STARTED
|
||||
plan.save(update_fields=["invoicing_status"])
|
||||
|
||||
# Invoice Variables
|
||||
stripe_invoice: stripe.Invoice | None = None
|
||||
need_to_invoice = False
|
||||
|
||||
# Track if we added renewal invoice item which is possibly eligible for discount.
|
||||
renewal_invoice_period: stripe.InvoiceItem.CreateParamsPeriod | None = None
|
||||
invoice_item_created = False
|
||||
invoice_period: stripe.InvoiceItem.CreateParamsPeriod | None = None
|
||||
for ledger_entry in LicenseLedger.objects.filter(
|
||||
plan=plan, id__gt=invoiced_through_id, event_time__lte=event_time
|
||||
).order_by("id"):
|
||||
# InvoiceItem variables.
|
||||
invoice_item_params = stripe.InvoiceItem.CreateParams(
|
||||
customer=plan.customer.stripe_customer_id
|
||||
)
|
||||
|
||||
price_args: PriceArgs = {}
|
||||
if ledger_entry.is_renewal:
|
||||
if plan.fixed_price is not None:
|
||||
amount_due = get_amount_due_fixed_price_plan(
|
||||
plan.fixed_price, plan.billing_schedule
|
||||
)
|
||||
invoice_item_params["amount"] = amount_due
|
||||
price_args = {"amount": amount_due}
|
||||
else:
|
||||
assert plan.price_per_license is not None # needed for mypy
|
||||
invoice_item_params["unit_amount_decimal"] = str(plan.price_per_license)
|
||||
invoice_item_params["quantity"] = ledger_entry.licenses
|
||||
invoice_item_params["description"] = f"{plan.name} - renewal"
|
||||
need_to_invoice = True
|
||||
price_args = {
|
||||
"unit_amount": plan.price_per_license,
|
||||
"quantity": ledger_entry.licenses,
|
||||
}
|
||||
description = f"{plan.name} - renewal"
|
||||
elif (
|
||||
plan.fixed_price is None
|
||||
and licenses_base is not None
|
||||
@@ -3285,67 +3264,44 @@ class BillingSession(ABC):
|
||||
plan_renewal_or_end_date - ledger_entry.event_time
|
||||
) / (billing_period_end - last_renewal)
|
||||
unit_amount = int(plan.price_per_license * proration_fraction + 0.5)
|
||||
invoice_item_params["unit_amount_decimal"] = str(unit_amount)
|
||||
invoice_item_params["quantity"] = ledger_entry.licenses - licenses_base
|
||||
invoice_item_params["description"] = "Additional license ({} - {})".format(
|
||||
price_args = {
|
||||
"unit_amount": unit_amount,
|
||||
"quantity": ledger_entry.licenses - licenses_base,
|
||||
}
|
||||
description = "Additional license ({} - {})".format(
|
||||
ledger_entry.event_time.strftime("%b %-d, %Y"),
|
||||
plan_renewal_or_end_date.strftime("%b %-d, %Y"),
|
||||
)
|
||||
need_to_invoice = True
|
||||
|
||||
if stripe_invoice is None and need_to_invoice:
|
||||
if plan.charge_automatically:
|
||||
collection_method: Literal["charge_automatically", "send_invoice"] = (
|
||||
"charge_automatically"
|
||||
)
|
||||
days_until_due = None
|
||||
else:
|
||||
collection_method = "send_invoice"
|
||||
days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE
|
||||
invoice_params = stripe.Invoice.CreateParams(
|
||||
auto_advance=True,
|
||||
collection_method=collection_method,
|
||||
customer=plan.customer.stripe_customer_id,
|
||||
statement_descriptor=plan.name,
|
||||
)
|
||||
if days_until_due is not None:
|
||||
invoice_params["days_until_due"] = days_until_due
|
||||
stripe_invoice = stripe.Invoice.create(**invoice_params)
|
||||
|
||||
if invoice_item_params.get("description") is not None:
|
||||
invoice_period = stripe.InvoiceItem.CreateParamsPeriod(
|
||||
start=datetime_to_timestamp(ledger_entry.event_time),
|
||||
end=datetime_to_timestamp(
|
||||
if price_args:
|
||||
plan.invoiced_through = ledger_entry
|
||||
plan.invoicing_status = CustomerPlan.INVOICING_STATUS_STARTED
|
||||
plan.save(update_fields=["invoicing_status", "invoiced_through"])
|
||||
invoice_period = {
|
||||
"start": datetime_to_timestamp(ledger_entry.event_time),
|
||||
"end": datetime_to_timestamp(
|
||||
get_plan_renewal_or_end_date(plan, ledger_entry.event_time)
|
||||
),
|
||||
}
|
||||
stripe.InvoiceItem.create(
|
||||
currency="usd",
|
||||
customer=plan.customer.stripe_customer_id,
|
||||
description=description,
|
||||
discountable=False,
|
||||
period=invoice_period,
|
||||
idempotency_key=get_idempotency_key(ledger_entry),
|
||||
**price_args,
|
||||
)
|
||||
|
||||
if ledger_entry.is_renewal:
|
||||
renewal_invoice_period = invoice_period
|
||||
|
||||
invoice_item_params["currency"] = "usd"
|
||||
invoice_item_params["discountable"] = False
|
||||
invoice_item_params["period"] = invoice_period
|
||||
invoice_item_params["idempotency_key"] = get_idempotency_key(ledger_entry)
|
||||
assert stripe_invoice is not None
|
||||
assert stripe_invoice.id is not None
|
||||
invoice_item_params["invoice"] = stripe_invoice.id
|
||||
stripe.InvoiceItem.create(**invoice_item_params)
|
||||
|
||||
# Update license base per ledger_entry.
|
||||
licenses_base = ledger_entry.licenses
|
||||
invoice_item_created = True
|
||||
plan.invoiced_through = ledger_entry
|
||||
plan.save(update_fields=["invoiced_through"])
|
||||
plan.invoicing_status = CustomerPlan.INVOICING_STATUS_DONE
|
||||
plan.save(update_fields=["invoicing_status", "invoiced_through"])
|
||||
licenses_base = ledger_entry.licenses
|
||||
|
||||
flat_discount, flat_discounted_months = self.get_flat_discount_info(plan.customer)
|
||||
if stripe_invoice is not None:
|
||||
# Only apply discount if this invoice contains renewal of the plan.
|
||||
if (
|
||||
renewal_invoice_period is not None
|
||||
and plan.fixed_price is None
|
||||
and flat_discounted_months > 0
|
||||
):
|
||||
assert stripe_invoice.id is not None
|
||||
if invoice_item_created:
|
||||
assert invoice_period is not None
|
||||
flat_discount, flat_discounted_months = self.get_flat_discount_info(plan.customer)
|
||||
if plan.fixed_price is None and flat_discounted_months > 0:
|
||||
num_months = (
|
||||
12 if plan.billing_schedule == CustomerPlan.BILLING_SCHEDULE_ANNUAL else 1
|
||||
)
|
||||
@@ -3354,27 +3310,36 @@ class BillingSession(ABC):
|
||||
plan.customer.flat_discounted_months -= flat_discounted_months
|
||||
plan.customer.save(update_fields=["flat_discounted_months"])
|
||||
stripe.InvoiceItem.create(
|
||||
invoice=stripe_invoice.id,
|
||||
currency="usd",
|
||||
customer=plan.customer.stripe_customer_id,
|
||||
description=f"${cents_to_dollar_string(flat_discount)}/month new customer discount",
|
||||
# Negative value to apply discount.
|
||||
amount=(-1 * discount),
|
||||
period=renewal_invoice_period,
|
||||
period=invoice_period,
|
||||
)
|
||||
|
||||
if plan.charge_automatically:
|
||||
collection_method: Literal["charge_automatically", "send_invoice"] = (
|
||||
"charge_automatically"
|
||||
)
|
||||
days_until_due = None
|
||||
else:
|
||||
collection_method = "send_invoice"
|
||||
days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE
|
||||
invoice_params = stripe.Invoice.CreateParams(
|
||||
auto_advance=True,
|
||||
collection_method=collection_method,
|
||||
customer=plan.customer.stripe_customer_id,
|
||||
statement_descriptor=plan.name,
|
||||
)
|
||||
if days_until_due is not None:
|
||||
invoice_params["days_until_due"] = days_until_due
|
||||
stripe_invoice = stripe.Invoice.create(**invoice_params)
|
||||
stripe.Invoice.finalize_invoice(stripe_invoice)
|
||||
|
||||
plan.invoicing_status = CustomerPlan.INVOICING_STATUS_DONE
|
||||
plan.next_invoice_date = next_invoice_date(plan)
|
||||
plan.stale_audit_log_data_email_sent = False
|
||||
plan.save(
|
||||
update_fields=[
|
||||
"next_invoice_date",
|
||||
"stale_audit_log_data_email_sent",
|
||||
"invoicing_status",
|
||||
]
|
||||
)
|
||||
plan.invoice_overdue_email_sent = False
|
||||
plan.save(update_fields=["next_invoice_date", "invoice_overdue_email_sent"])
|
||||
|
||||
def do_change_plan_to_new_tier(self, new_plan_tier: int) -> str:
|
||||
customer = self.get_customer()
|
||||
@@ -3677,6 +3642,35 @@ class BillingSession(ABC):
|
||||
assert isinstance(stripe_invoice.customer, str)
|
||||
assert stripe_invoice.statement_descriptor is not None
|
||||
assert stripe_invoice.metadata is not None
|
||||
invoice_items = stripe_invoice.lines.data
|
||||
# Stripe does something weird and puts the discount item first, so we need to reverse the order here.
|
||||
invoice_items.reverse()
|
||||
for invoice_item in invoice_items:
|
||||
assert invoice_item.description is not None
|
||||
price_args: PriceArgs = {}
|
||||
# If amount is positive, this must be non-discount item we need to update.
|
||||
if invoice_item.amount > 0:
|
||||
assert invoice_item.price is not None
|
||||
assert invoice_item.price.unit_amount is not None
|
||||
price_args = {
|
||||
"quantity": licenses,
|
||||
"unit_amount": invoice_item.price.unit_amount,
|
||||
}
|
||||
else:
|
||||
price_args = {
|
||||
"amount": invoice_item.amount,
|
||||
}
|
||||
stripe.InvoiceItem.create(
|
||||
currency=invoice_item.currency,
|
||||
customer=stripe_invoice.customer,
|
||||
description=invoice_item.description,
|
||||
period={
|
||||
"start": invoice_item.period.start,
|
||||
"end": invoice_item.period.end,
|
||||
},
|
||||
**price_args,
|
||||
)
|
||||
|
||||
assert plan.next_invoice_date is not None
|
||||
# Difference between end of free trial and event time
|
||||
days_until_due = (plan.next_invoice_date - event_time).days
|
||||
@@ -3689,38 +3683,6 @@ class BillingSession(ABC):
|
||||
statement_descriptor=stripe_invoice.statement_descriptor,
|
||||
metadata=stripe_invoice.metadata,
|
||||
)
|
||||
assert new_stripe_invoice.id is not None
|
||||
|
||||
invoice_items = stripe_invoice.lines.data
|
||||
# Stripe does something weird and puts the discount item first, so we need to reverse the order here.
|
||||
invoice_items.reverse()
|
||||
for invoice_item in invoice_items:
|
||||
assert invoice_item.description is not None
|
||||
price_args: PriceArgs = {}
|
||||
# If amount is positive, this must be non-discount item we need to update.
|
||||
if invoice_item.amount > 0:
|
||||
assert invoice_item.pricing is not None
|
||||
assert invoice_item.pricing.unit_amount_decimal is not None
|
||||
price_args = {
|
||||
"quantity": licenses,
|
||||
"unit_amount_decimal": str(invoice_item.pricing.unit_amount_decimal),
|
||||
}
|
||||
else:
|
||||
price_args = {
|
||||
"amount": invoice_item.amount,
|
||||
}
|
||||
stripe.InvoiceItem.create(
|
||||
invoice=new_stripe_invoice.id,
|
||||
currency=invoice_item.currency,
|
||||
customer=stripe_invoice.customer,
|
||||
description=invoice_item.description,
|
||||
period={
|
||||
"start": invoice_item.period.start,
|
||||
"end": invoice_item.period.end,
|
||||
},
|
||||
**price_args,
|
||||
)
|
||||
|
||||
new_stripe_invoice = stripe.Invoice.finalize_invoice(new_stripe_invoice)
|
||||
last_sent_invoice.stripe_invoice_id = str(new_stripe_invoice.id)
|
||||
last_sent_invoice.save(update_fields=["stripe_invoice_id"])
|
||||
@@ -4235,6 +4197,8 @@ class RealmBillingSession(BillingSession):
|
||||
return UpgradePageSessionTypeSpecificContext(
|
||||
customer_name=self.realm.name,
|
||||
email=self.get_email(),
|
||||
is_demo_organization=self.realm.demo_organization_scheduled_deletion_date is not None,
|
||||
demo_organization_scheduled_deletion_date=self.realm.demo_organization_scheduled_deletion_date,
|
||||
is_self_hosting=False,
|
||||
)
|
||||
|
||||
@@ -4632,6 +4596,8 @@ class RemoteRealmBillingSession(BillingSession):
|
||||
return UpgradePageSessionTypeSpecificContext(
|
||||
customer_name=self.remote_realm.host,
|
||||
email=self.get_email(),
|
||||
is_demo_organization=False,
|
||||
demo_organization_scheduled_deletion_date=None,
|
||||
is_self_hosting=True,
|
||||
)
|
||||
|
||||
@@ -5097,6 +5063,8 @@ class RemoteServerBillingSession(BillingSession):
|
||||
return UpgradePageSessionTypeSpecificContext(
|
||||
customer_name=self.remote_server.hostname,
|
||||
email=self.get_email(),
|
||||
is_demo_organization=False,
|
||||
demo_organization_scheduled_deletion_date=None,
|
||||
is_self_hosting=True,
|
||||
)
|
||||
|
||||
@@ -5445,180 +5413,90 @@ def get_plan_renewal_or_end_date(plan: CustomerPlan, event_time: datetime) -> da
|
||||
return billing_period_end
|
||||
|
||||
|
||||
def maybe_send_fixed_price_plan_renewal_reminder_email(
|
||||
plan: CustomerPlan, billing_session: BillingSession
|
||||
) -> None:
|
||||
# We expect to have both an end date and next invoice date
|
||||
# for this CustomerPlan.
|
||||
assert plan.end_date is not None
|
||||
assert plan.next_invoice_date is not None
|
||||
# The max gap between two months is 62 days (1 Jul - 1 Sep).
|
||||
if plan.end_date - plan.next_invoice_date <= timedelta(days=62):
|
||||
context = {
|
||||
"billing_entity": billing_session.billing_entity_display_name,
|
||||
"end_date": plan.end_date.strftime("%Y-%m-%d"),
|
||||
"support_url": billing_session.support_url(),
|
||||
"notice_reason": "fixed_price_plan_ends_soon",
|
||||
}
|
||||
send_email(
|
||||
"zerver/emails/internal_billing_notice",
|
||||
to_emails=[BILLING_SUPPORT_EMAIL],
|
||||
from_address=FromAddress.tokenized_no_reply_address(),
|
||||
context=context,
|
||||
)
|
||||
plan.reminder_to_review_plan_email_sent = True
|
||||
plan.save(update_fields=["reminder_to_review_plan_email_sent"])
|
||||
|
||||
|
||||
def maybe_send_stale_audit_log_data_email(
|
||||
plan: CustomerPlan,
|
||||
billing_session: BillingSession,
|
||||
next_invoice_date: datetime,
|
||||
last_audit_log_update: datetime | None,
|
||||
) -> None:
|
||||
if last_audit_log_update is None: # nocoverage
|
||||
# We have no audit log data from the remote server at all,
|
||||
# and they have a paid plan or scheduled upgrade, so email
|
||||
# billing support.
|
||||
context = {
|
||||
"billing_entity": billing_session.billing_entity_display_name,
|
||||
"support_url": billing_session.support_url(),
|
||||
"last_audit_log_update": "Never uploaded",
|
||||
"notice_reason": "stale_audit_log_data",
|
||||
}
|
||||
send_email(
|
||||
"zerver/emails/internal_billing_notice",
|
||||
to_emails=[BILLING_SUPPORT_EMAIL],
|
||||
from_address=FromAddress.tokenized_no_reply_address(),
|
||||
context=context,
|
||||
)
|
||||
plan.stale_audit_log_data_email_sent = True
|
||||
plan.save(update_fields=["stale_audit_log_data_email_sent"])
|
||||
return
|
||||
|
||||
if next_invoice_date - last_audit_log_update < timedelta(days=1): # nocoverage
|
||||
# If it's been less than a day since the last audit log update,
|
||||
# then don't email billing support as the issue with the remote
|
||||
# server could be transient.
|
||||
return
|
||||
|
||||
context = {
|
||||
"billing_entity": billing_session.billing_entity_display_name,
|
||||
"support_url": billing_session.support_url(),
|
||||
"last_audit_log_update": last_audit_log_update.strftime("%Y-%m-%d"),
|
||||
"notice_reason": "stale_audit_log_data",
|
||||
}
|
||||
send_email(
|
||||
"zerver/emails/internal_billing_notice",
|
||||
to_emails=[BILLING_SUPPORT_EMAIL],
|
||||
from_address=FromAddress.tokenized_no_reply_address(),
|
||||
context=context,
|
||||
)
|
||||
plan.stale_audit_log_data_email_sent = True
|
||||
plan.save(update_fields=["stale_audit_log_data_email_sent"])
|
||||
|
||||
|
||||
def check_remote_server_audit_log_data(
|
||||
remote_server: RemoteZulipServer, plan: CustomerPlan, billing_session: BillingSession
|
||||
) -> bool:
|
||||
# If this is a complimentary access plan without an upgrade scheduled,
|
||||
# we do not need the remote server's audit log data to downgrade the plan.
|
||||
if plan.is_complimentary_access_plan() and plan.status == CustomerPlan.ACTIVE:
|
||||
return True
|
||||
|
||||
# We expect to have a next invoice date for this CustomerPlan.
|
||||
assert plan.next_invoice_date is not None
|
||||
next_invoice_date = plan.next_invoice_date
|
||||
last_audit_log_update = remote_server.last_audit_log_update
|
||||
if last_audit_log_update is None or next_invoice_date > last_audit_log_update:
|
||||
if not plan.stale_audit_log_data_email_sent:
|
||||
maybe_send_stale_audit_log_data_email(
|
||||
plan, billing_session, next_invoice_date, last_audit_log_update
|
||||
)
|
||||
|
||||
# We still process free trial plans so that we can directly downgrade them.
|
||||
if plan.is_free_trial() and not plan.charge_automatically: # nocoverage
|
||||
return True
|
||||
|
||||
# We don't have current audit log data from the remote server,
|
||||
# so we don't have enough information to invoice the plan.
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def review_and_maybe_invoice_plan(
|
||||
plan: CustomerPlan,
|
||||
event_time: datetime,
|
||||
) -> None:
|
||||
remote_server: RemoteZulipServer | None = None
|
||||
if plan.customer.realm is not None:
|
||||
billing_session: BillingSession = RealmBillingSession(realm=plan.customer.realm)
|
||||
elif plan.customer.remote_realm is not None:
|
||||
remote_realm = plan.customer.remote_realm
|
||||
remote_server = remote_realm.server
|
||||
billing_session = RemoteRealmBillingSession(remote_realm=remote_realm)
|
||||
elif plan.customer.remote_server is not None:
|
||||
remote_server = plan.customer.remote_server
|
||||
billing_session = RemoteServerBillingSession(remote_server=remote_server)
|
||||
|
||||
if (
|
||||
plan.fixed_price is not None
|
||||
and plan.end_date is not None
|
||||
and not plan.reminder_to_review_plan_email_sent
|
||||
):
|
||||
maybe_send_fixed_price_plan_renewal_reminder_email(plan, billing_session)
|
||||
|
||||
try_to_invoice_plan = True
|
||||
if remote_server:
|
||||
# We need the audit log data from the remote server to be
|
||||
# current enough for license checks on paid plans.
|
||||
try_to_invoice_plan = check_remote_server_audit_log_data(
|
||||
remote_server, plan, billing_session
|
||||
)
|
||||
|
||||
if try_to_invoice_plan:
|
||||
# plan.next_invoice_date can be None after calling invoice_plan.
|
||||
while plan.next_invoice_date is not None and plan.next_invoice_date <= event_time:
|
||||
billing_session.invoice_plan(plan, plan.next_invoice_date)
|
||||
plan.refresh_from_db()
|
||||
|
||||
|
||||
def invoice_plans_as_needed(event_time: datetime | None = None) -> None:
|
||||
failed_customer_ids = set()
|
||||
if event_time is None:
|
||||
event_time = timezone_now()
|
||||
# For complimentary access plans with status SWITCH_PLAN_TIER_AT_PLAN_END, we need
|
||||
# to invoice the complimentary access plan followed by the new plan on the same day,
|
||||
# hence ordering by ID.
|
||||
for plan in CustomerPlan.objects.filter(next_invoice_date__lte=event_time).order_by("id"):
|
||||
if plan.customer.id in failed_customer_ids: # nocoverage
|
||||
# We've already had a failure for this customer in this
|
||||
# invoicing attempt; skip it so we can process others,
|
||||
# without incorrectly processing other plans on this same
|
||||
# customer.
|
||||
continue
|
||||
remote_server: RemoteZulipServer | None = None
|
||||
if plan.customer.realm is not None:
|
||||
billing_session: BillingSession = RealmBillingSession(realm=plan.customer.realm)
|
||||
elif plan.customer.remote_realm is not None:
|
||||
remote_realm = plan.customer.remote_realm
|
||||
remote_server = remote_realm.server
|
||||
billing_session = RemoteRealmBillingSession(remote_realm=remote_realm)
|
||||
elif plan.customer.remote_server is not None:
|
||||
remote_server = plan.customer.remote_server
|
||||
billing_session = RemoteServerBillingSession(remote_server=remote_server)
|
||||
|
||||
try:
|
||||
review_and_maybe_invoice_plan(plan, event_time)
|
||||
except Exception as e:
|
||||
failed_customer_ids.add(plan.customer.id)
|
||||
if isinstance(e, BillingError):
|
||||
billing_logger.exception(
|
||||
"Invoicing failed: Customer.id: %s, CustomerPlan.id: %s, BillingError: %s",
|
||||
plan.customer.id,
|
||||
plan.id,
|
||||
e.error_description,
|
||||
)
|
||||
elif isinstance(e, AssertionError): # nocoverage
|
||||
billing_logger.exception(
|
||||
"Invoicing failed due to AssertionError: Customer.id: %s, CustomerPlan.id: %s",
|
||||
plan.customer.id,
|
||||
plan.id,
|
||||
stack_info=True,
|
||||
)
|
||||
else:
|
||||
billing_logger.exception(e, stack_info=True) # nocoverage
|
||||
assert plan.next_invoice_date is not None # for mypy
|
||||
|
||||
if (
|
||||
plan.fixed_price is not None
|
||||
and not plan.reminder_to_review_plan_email_sent
|
||||
and plan.end_date is not None # for mypy
|
||||
# The max gap between two months is 62 days. (1 Jul - 1 Sep)
|
||||
and plan.end_date - plan.next_invoice_date <= timedelta(days=62)
|
||||
):
|
||||
context = {
|
||||
"billing_entity": billing_session.billing_entity_display_name,
|
||||
"end_date": plan.end_date.strftime("%Y-%m-%d"),
|
||||
"support_url": billing_session.support_url(),
|
||||
"notice_reason": "fixed_price_plan_ends_soon",
|
||||
}
|
||||
send_email(
|
||||
"zerver/emails/internal_billing_notice",
|
||||
to_emails=[BILLING_SUPPORT_EMAIL],
|
||||
from_address=FromAddress.tokenized_no_reply_address(),
|
||||
context=context,
|
||||
)
|
||||
plan.reminder_to_review_plan_email_sent = True
|
||||
plan.save(update_fields=["reminder_to_review_plan_email_sent"])
|
||||
|
||||
if remote_server:
|
||||
free_plan_with_no_next_plan = (
|
||||
not plan.is_a_paid_plan() and plan.status == CustomerPlan.ACTIVE
|
||||
)
|
||||
free_trial_pay_by_invoice_plan = plan.is_free_trial() and not plan.charge_automatically
|
||||
last_audit_log_update = remote_server.last_audit_log_update
|
||||
if not free_plan_with_no_next_plan and (
|
||||
last_audit_log_update is None or plan.next_invoice_date > last_audit_log_update
|
||||
):
|
||||
if (
|
||||
last_audit_log_update is None
|
||||
or plan.next_invoice_date - last_audit_log_update >= timedelta(days=1)
|
||||
) and not plan.invoice_overdue_email_sent:
|
||||
last_audit_log_update_string = "Never uploaded"
|
||||
if last_audit_log_update is not None:
|
||||
last_audit_log_update_string = last_audit_log_update.strftime("%Y-%m-%d")
|
||||
context = {
|
||||
"billing_entity": billing_session.billing_entity_display_name,
|
||||
"support_url": billing_session.support_url(),
|
||||
"last_audit_log_update": last_audit_log_update_string,
|
||||
"notice_reason": "invoice_overdue",
|
||||
}
|
||||
send_email(
|
||||
"zerver/emails/internal_billing_notice",
|
||||
to_emails=[BILLING_SUPPORT_EMAIL],
|
||||
from_address=FromAddress.tokenized_no_reply_address(),
|
||||
context=context,
|
||||
)
|
||||
plan.invoice_overdue_email_sent = True
|
||||
plan.save(update_fields=["invoice_overdue_email_sent"])
|
||||
|
||||
# We still process free trial plans so that we can directly downgrade them.
|
||||
# Above emails can serve as a reminder to followup for additional feedback.
|
||||
if not free_trial_pay_by_invoice_plan:
|
||||
continue
|
||||
|
||||
while (
|
||||
plan.next_invoice_date is not None # type: ignore[redundant-expr] # plan.next_invoice_date can be None after calling invoice_plan.
|
||||
and plan.next_invoice_date <= event_time
|
||||
):
|
||||
billing_session.invoice_plan(plan, plan.next_invoice_date)
|
||||
plan.refresh_from_db()
|
||||
|
||||
|
||||
def is_realm_on_free_trial(realm: Realm) -> bool:
|
||||
@@ -5685,8 +5563,7 @@ def downgrade_small_realms_behind_on_payments_as_needed() -> None:
|
||||
billing_session.void_all_open_invoices()
|
||||
context: dict[str, str | Realm] = {
|
||||
"upgrade_url": f"{realm.url}{reverse('upgrade_page')}",
|
||||
"realm_url": realm.url,
|
||||
"string_id": realm.string_id,
|
||||
"realm": realm,
|
||||
}
|
||||
send_email_to_users_with_billing_access_and_realm_owners(
|
||||
"zerver/emails/realm_auto_downgraded",
|
||||
|
@@ -13,9 +13,14 @@ from corporate.lib.stripe import (
|
||||
RemoteServerBillingSession,
|
||||
get_configured_fixed_price_plan_offer,
|
||||
)
|
||||
from corporate.models.customers import Customer
|
||||
from corporate.models.plans import CustomerPlan, get_current_plan_by_customer
|
||||
from corporate.models.stripe_state import Event, Invoice, Session
|
||||
from corporate.models import (
|
||||
Customer,
|
||||
CustomerPlan,
|
||||
Event,
|
||||
Invoice,
|
||||
Session,
|
||||
get_current_plan_by_customer,
|
||||
)
|
||||
from zerver.lib.send_email import FromAddress, send_email
|
||||
from zerver.models.users import get_active_user_profile_by_id_in_realm
|
||||
|
||||
|
@@ -17,13 +17,17 @@ from corporate.lib.stripe import (
|
||||
get_push_status_for_remote_request,
|
||||
start_of_next_billing_cycle,
|
||||
)
|
||||
from corporate.models.customers import Customer
|
||||
from corporate.models.licenses import LicenseLedger
|
||||
from corporate.models.plans import CustomerPlan, CustomerPlanOffer, get_current_plan_by_customer
|
||||
from corporate.models.sponsorships import ZulipSponsorshipRequest
|
||||
from corporate.models import (
|
||||
Customer,
|
||||
CustomerPlan,
|
||||
CustomerPlanOffer,
|
||||
LicenseLedger,
|
||||
ZulipSponsorshipRequest,
|
||||
get_current_plan_by_customer,
|
||||
)
|
||||
from zerver.lib.timestamp import timestamp_to_datetime
|
||||
from zerver.models import Realm
|
||||
from zerver.models.realm_audit_logs import AuditLogEventType, RealmAuditLog
|
||||
from zerver.models.realm_audit_logs import AuditLogEventType
|
||||
from zerver.models.realms import get_org_type_display_name
|
||||
from zilencer.lib.remote_counts import MissingDataError
|
||||
from zilencer.models import (
|
||||
@@ -126,33 +130,12 @@ class CloudSupportData:
|
||||
plan_data: PlanData
|
||||
sponsorship_data: SponsorshipData
|
||||
user_data: UserData
|
||||
file_upload_usage: str
|
||||
is_scrubbed: bool
|
||||
|
||||
|
||||
def get_stripe_customer_url(stripe_id: str) -> str:
|
||||
return f"https://dashboard.stripe.com/customers/{stripe_id}" # nocoverage
|
||||
|
||||
|
||||
def get_formatted_realm_upload_space_used(realm: Realm) -> str: # nocoverage
|
||||
realm_bytes_used = realm.currently_used_upload_space_bytes()
|
||||
files_uploaded = realm_bytes_used > 0
|
||||
|
||||
realm_uploads = "No uploads"
|
||||
if files_uploaded:
|
||||
realm_uploads = str(round(realm_bytes_used / 1024 / 1024, 2))
|
||||
|
||||
quota = realm.upload_quota_bytes()
|
||||
if quota is None:
|
||||
if files_uploaded:
|
||||
return f"{realm_uploads} MiB / No quota"
|
||||
return f"{realm_uploads} / No quota"
|
||||
if quota == 0:
|
||||
return f"{realm_uploads} / 0.0 MiB"
|
||||
quota_mb = round(quota / 1024 / 1024, 2)
|
||||
return f"{realm_uploads} / {quota_mb} MiB"
|
||||
|
||||
|
||||
def get_realm_user_data(realm: Realm) -> UserData:
|
||||
non_guests = get_non_guest_user_count(realm)
|
||||
guests = get_guest_user_count(realm)
|
||||
@@ -498,8 +481,4 @@ def get_data_for_cloud_support_view(billing_session: BillingSession) -> CloudSup
|
||||
plan_data=plan_data,
|
||||
sponsorship_data=sponsorship_data,
|
||||
user_data=user_data,
|
||||
file_upload_usage=get_formatted_realm_upload_space_used(billing_session.realm),
|
||||
is_scrubbed=RealmAuditLog.objects.filter(
|
||||
realm=billing_session.realm, event_type=AuditLogEventType.REALM_SCRUBBED
|
||||
).exists(),
|
||||
)
|
||||
|
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 5.1.8 on 2025-05-27 14:26
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("corporate", "0045_zulipsponsorshiprequest_plan_to_use_zulip"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="customerplan",
|
||||
old_name="invoice_overdue_email_sent",
|
||||
new_name="stale_audit_log_data_email_sent",
|
||||
),
|
||||
]
|
587
corporate/models.py
Normal file
587
corporate/models.py
Normal file
@@ -0,0 +1,587 @@
|
||||
from enum import Enum
|
||||
from typing import Any, 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, SET_NULL, Q
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.models import Realm, UserProfile
|
||||
from zilencer.models import RemoteRealm, RemoteZulipServer
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
"""
|
||||
This model primarily serves to connect a Realm with
|
||||
the corresponding Stripe customer object for payment purposes
|
||||
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)
|
||||
|
||||
# Discounted price for required_plan_tier in cents.
|
||||
# We treat 0 as no discount. Not using `null` here keeps the
|
||||
# checks simpler and avoids the cases where we forget to
|
||||
# check for both `null` and 0.
|
||||
monthly_discounted_price = models.IntegerField(default=0, null=False)
|
||||
annual_discounted_price = models.IntegerField(default=0, null=False)
|
||||
|
||||
minimum_licenses = models.PositiveIntegerField(null=True)
|
||||
# Used for limiting discounted price or a fixed_price
|
||||
# to be used only for a particular CustomerPlan tier.
|
||||
required_plan_tier = models.SmallIntegerField(null=True)
|
||||
# Some non-profit organizations on manual license management pay
|
||||
# only for their paid employees. We don't prevent these
|
||||
# organizations from adding more users than the number of licenses
|
||||
# they purchased.
|
||||
exempt_from_license_number_check = models.BooleanField(default=False)
|
||||
|
||||
# In cents.
|
||||
flat_discount = models.IntegerField(default=2000)
|
||||
# Number of months left in the flat discount period.
|
||||
flat_discounted_months = models.IntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
# Enforce that at least one of these is set.
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
condition=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})"
|
||||
elif self.remote_realm is not None:
|
||||
return f"{self.remote_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})"
|
||||
|
||||
def get_discounted_price_for_plan(self, plan_tier: int, schedule: int) -> int | None:
|
||||
if plan_tier != self.required_plan_tier:
|
||||
return None
|
||||
|
||||
if schedule == CustomerPlan.BILLING_SCHEDULE_ANNUAL:
|
||||
return self.annual_discounted_price
|
||||
|
||||
assert schedule == CustomerPlan.BILLING_SCHEDULE_MONTHLY
|
||||
return self.monthly_discounted_price
|
||||
|
||||
|
||||
def get_customer_by_realm(realm: Realm) -> Customer | None:
|
||||
return Customer.objects.filter(realm=realm).first()
|
||||
|
||||
|
||||
def get_customer_by_remote_server(remote_server: RemoteZulipServer) -> Customer | None:
|
||||
return Customer.objects.filter(remote_server=remote_server).first()
|
||||
|
||||
|
||||
def get_customer_by_remote_realm(remote_realm: RemoteRealm) -> Customer | None: # nocoverage
|
||||
return Customer.objects.filter(remote_realm=remote_realm).first()
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
stripe_event_id = models.CharField(max_length=255)
|
||||
|
||||
type = models.CharField(max_length=255)
|
||||
|
||||
RECEIVED = 1
|
||||
EVENT_HANDLER_STARTED = 30
|
||||
EVENT_HANDLER_FAILED = 40
|
||||
EVENT_HANDLER_SUCCEEDED = 50
|
||||
status = models.SmallIntegerField(default=RECEIVED)
|
||||
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField(db_index=True)
|
||||
content_object = GenericForeignKey("content_type", "object_id")
|
||||
|
||||
handler_error = models.JSONField(default=None, null=True)
|
||||
|
||||
def get_event_handler_details_as_dict(self) -> dict[str, Any]:
|
||||
details_dict = {}
|
||||
details_dict["status"] = {
|
||||
Event.RECEIVED: "not_started",
|
||||
Event.EVENT_HANDLER_STARTED: "started",
|
||||
Event.EVENT_HANDLER_FAILED: "failed",
|
||||
Event.EVENT_HANDLER_SUCCEEDED: "succeeded",
|
||||
}[self.status]
|
||||
if self.handler_error:
|
||||
details_dict["error"] = self.handler_error
|
||||
return details_dict
|
||||
|
||||
|
||||
def get_last_associated_event_by_type(
|
||||
content_object: Union["Invoice", "PaymentIntent", "Session"], event_type: str
|
||||
) -> Event | None:
|
||||
content_type = ContentType.objects.get_for_model(type(content_object))
|
||||
return Event.objects.filter(
|
||||
content_type=content_type, object_id=content_object.id, type=event_type
|
||||
).last()
|
||||
|
||||
|
||||
class Session(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
||||
stripe_session_id = models.CharField(max_length=255, unique=True)
|
||||
|
||||
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)
|
||||
|
||||
# CustomerPlan tier that the user is upgrading to.
|
||||
tier = models.SmallIntegerField(null=True)
|
||||
|
||||
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.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]:
|
||||
session_dict: dict[str, Any] = {}
|
||||
|
||||
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
|
||||
)
|
||||
session_dict["tier"] = self.tier
|
||||
event = self.get_last_associated_event()
|
||||
if event is not None:
|
||||
session_dict["event_handler"] = event.get_event_handler_details_as_dict()
|
||||
return session_dict
|
||||
|
||||
def get_last_associated_event(self) -> Event | None:
|
||||
if self.status == Session.CREATED:
|
||||
return None
|
||||
return get_last_associated_event_by_type(self, "checkout.session.completed")
|
||||
|
||||
|
||||
class PaymentIntent(models.Model): # nocoverage
|
||||
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
||||
stripe_payment_intent_id = models.CharField(max_length=255, unique=True)
|
||||
|
||||
REQUIRES_PAYMENT_METHOD = 1
|
||||
REQUIRES_CONFIRMATION = 20
|
||||
REQUIRES_ACTION = 30
|
||||
PROCESSING = 40
|
||||
REQUIRES_CAPTURE = 50
|
||||
CANCELLED = 60
|
||||
SUCCEEDED = 70
|
||||
|
||||
status = models.SmallIntegerField()
|
||||
last_payment_error = models.JSONField(default=None, null=True)
|
||||
|
||||
@classmethod
|
||||
def get_status_integer_from_status_text(cls, status_text: str) -> int:
|
||||
return getattr(cls, status_text.upper())
|
||||
|
||||
def get_status_as_string(self) -> str:
|
||||
return {
|
||||
PaymentIntent.REQUIRES_PAYMENT_METHOD: "requires_payment_method",
|
||||
PaymentIntent.REQUIRES_CONFIRMATION: "requires_confirmation",
|
||||
PaymentIntent.REQUIRES_ACTION: "requires_action",
|
||||
PaymentIntent.PROCESSING: "processing",
|
||||
PaymentIntent.REQUIRES_CAPTURE: "requires_capture",
|
||||
PaymentIntent.CANCELLED: "cancelled",
|
||||
PaymentIntent.SUCCEEDED: "succeeded",
|
||||
}[self.status]
|
||||
|
||||
def get_last_associated_event(self) -> Event | None:
|
||||
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
|
||||
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 event is not None:
|
||||
payment_intent_dict["event_handler"] = event.get_event_handler_details_as_dict()
|
||||
return payment_intent_dict
|
||||
|
||||
|
||||
class Invoice(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
||||
stripe_invoice_id = models.CharField(max_length=255, unique=True)
|
||||
plan = models.ForeignKey("CustomerPlan", null=True, default=None, on_delete=SET_NULL)
|
||||
is_created_for_free_trial_upgrade = models.BooleanField(default=False)
|
||||
|
||||
SENT = 1
|
||||
PAID = 2
|
||||
VOID = 3
|
||||
status = models.SmallIntegerField()
|
||||
|
||||
def get_status_as_string(self) -> str:
|
||||
return {
|
||||
Invoice.SENT: "sent",
|
||||
Invoice.PAID: "paid",
|
||||
Invoice.VOID: "void",
|
||||
}[self.status]
|
||||
|
||||
def get_last_associated_event(self) -> Event | None:
|
||||
if self.status == Invoice.PAID:
|
||||
event_type = "invoice.paid"
|
||||
# TODO: Add test for this case. Not sure how to trigger naturally.
|
||||
else: # nocoverage
|
||||
return None # nocoverage
|
||||
return get_last_associated_event_by_type(self, event_type)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
stripe_invoice_dict: dict[str, Any] = {}
|
||||
stripe_invoice_dict["status"] = self.get_status_as_string()
|
||||
event = self.get_last_associated_event()
|
||||
if event is not None:
|
||||
stripe_invoice_dict["event_handler"] = event.get_event_handler_details_as_dict()
|
||||
return stripe_invoice_dict
|
||||
|
||||
|
||||
class AbstractCustomerPlan(models.Model):
|
||||
# A customer can only have one ACTIVE / CONFIGURED plan,
|
||||
# but old, inactive / processed plans are preserved to allow
|
||||
# auditing - so there can be multiple CustomerPlan / CustomerPlanOffer
|
||||
# objects pointing to one Customer.
|
||||
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
||||
|
||||
fixed_price = models.IntegerField(null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class CustomerPlanOffer(AbstractCustomerPlan):
|
||||
"""
|
||||
This is for storing offers configured via /support which
|
||||
the customer is yet to buy or schedule a purchase.
|
||||
|
||||
Once customer buys or schedules a purchase, we create a
|
||||
CustomerPlan record. The record in this table stays for
|
||||
audit purpose with status=PROCESSED.
|
||||
"""
|
||||
|
||||
TIER_CLOUD_STANDARD = 1
|
||||
TIER_CLOUD_PLUS = 2
|
||||
TIER_SELF_HOSTED_BASIC = 103
|
||||
TIER_SELF_HOSTED_BUSINESS = 104
|
||||
tier = models.SmallIntegerField()
|
||||
|
||||
# Whether the offer is:
|
||||
# * only configured
|
||||
# * processed by the customer to buy or schedule a purchase.
|
||||
CONFIGURED = 1
|
||||
PROCESSED = 2
|
||||
status = models.SmallIntegerField()
|
||||
|
||||
# ID of invoice sent when chose to 'Pay by invoice'.
|
||||
sent_invoice_id = models.CharField(max_length=255, null=True)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name} (status: {self.get_plan_status_as_text()})"
|
||||
|
||||
def get_plan_status_as_text(self) -> str:
|
||||
return {
|
||||
self.CONFIGURED: "Configured",
|
||||
self.PROCESSED: "Processed",
|
||||
}[self.status]
|
||||
|
||||
@staticmethod
|
||||
def name_from_tier(tier: int) -> str:
|
||||
return {
|
||||
CustomerPlanOffer.TIER_CLOUD_STANDARD: "Zulip Cloud Standard",
|
||||
CustomerPlanOffer.TIER_CLOUD_PLUS: "Zulip Cloud Plus",
|
||||
CustomerPlanOffer.TIER_SELF_HOSTED_BASIC: "Zulip Basic",
|
||||
CustomerPlanOffer.TIER_SELF_HOSTED_BUSINESS: "Zulip Business",
|
||||
}[tier]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.name_from_tier(self.tier)
|
||||
|
||||
|
||||
class CustomerPlan(AbstractCustomerPlan):
|
||||
"""
|
||||
This is for storing most of the fiddly details
|
||||
of the customer's plan.
|
||||
"""
|
||||
|
||||
automanage_licenses = models.BooleanField(default=False)
|
||||
charge_automatically = models.BooleanField(default=False)
|
||||
|
||||
# Both of the price_per_license and fixed_price are in cents. Exactly
|
||||
# one of them should be set. fixed_price is only for manual deals, and
|
||||
# can't be set via the self-serve billing system.
|
||||
price_per_license = models.IntegerField(null=True)
|
||||
|
||||
# Discount for current `billing_schedule`. For display purposes only.
|
||||
# Explicitly set to be TextField to avoid being used in calculations.
|
||||
# NOTE: This discount can be different for annual and monthly schedules.
|
||||
discount = models.TextField(null=True)
|
||||
|
||||
# Initialized with the time of plan creation. Used for calculating
|
||||
# start of next billing cycle, next invoice date etc. This value
|
||||
# should never be modified. The only exception is when we change
|
||||
# the status of the plan from free trial to active and reset the
|
||||
# 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",
|
||||
}
|
||||
billing_schedule = models.SmallIntegerField()
|
||||
|
||||
# The next date the billing system should go through ledger
|
||||
# entries and create invoices for additional users or plan
|
||||
# renewal. Since we use a daily cron job for invoicing, the
|
||||
# invoice will be generated the first time the cron job runs after
|
||||
# next_invoice_date.
|
||||
next_invoice_date = models.DateTimeField(db_index=True, null=True)
|
||||
|
||||
# Flag to track if an email has been sent to Zulip team for
|
||||
# invoice overdue by >= one day. Helps to send an email only once
|
||||
# and not every time when cron run.
|
||||
invoice_overdue_email_sent = models.BooleanField(default=False)
|
||||
|
||||
# Flag to track if an email has been sent to Zulip team to
|
||||
# review the pricing, 60 days before the end date. Helps to send
|
||||
# an email only once and not every time when cron run.
|
||||
reminder_to_review_plan_email_sent = models.BooleanField(default=False)
|
||||
|
||||
# On next_invoice_date, we go through ledger entries that were
|
||||
# created after invoiced_through and process them by generating
|
||||
# invoices for any additional users and/or plan renewal. Once the
|
||||
# invoice is generated, we update the value of invoiced_through
|
||||
# and set it to the last ledger entry we processed.
|
||||
invoiced_through = models.ForeignKey(
|
||||
"LicenseLedger", null=True, on_delete=CASCADE, related_name="+"
|
||||
)
|
||||
end_date = models.DateTimeField(null=True)
|
||||
|
||||
INVOICING_STATUS_DONE = 1
|
||||
INVOICING_STATUS_STARTED = 2
|
||||
INVOICING_STATUS_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)
|
||||
|
||||
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_BASIC = 103
|
||||
TIER_SELF_HOSTED_BUSINESS = 104
|
||||
TIER_SELF_HOSTED_ENTERPRISE = 105
|
||||
tier = models.SmallIntegerField()
|
||||
|
||||
PAID_PLAN_TIERS = [
|
||||
TIER_CLOUD_STANDARD,
|
||||
TIER_CLOUD_PLUS,
|
||||
TIER_SELF_HOSTED_BASIC,
|
||||
TIER_SELF_HOSTED_BUSINESS,
|
||||
TIER_SELF_HOSTED_ENTERPRISE,
|
||||
]
|
||||
|
||||
COMPLIMENTARY_PLAN_TIERS = [TIER_SELF_HOSTED_LEGACY]
|
||||
|
||||
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
|
||||
# "Live" plans should have a value < LIVE_STATUS_THRESHOLD.
|
||||
# There should be at most one live plan per customer.
|
||||
LIVE_STATUS_THRESHOLD = 10
|
||||
ENDED = 11
|
||||
NEVER_STARTED = 12
|
||||
status = models.SmallIntegerField(default=ACTIVE)
|
||||
|
||||
# Currently, all the fixed-price plans are configured for one year.
|
||||
# In future, we might change this to a field.
|
||||
FIXED_PRICE_PLAN_DURATION_MONTHS = 12
|
||||
|
||||
# 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_BASIC: "Zulip Basic",
|
||||
CustomerPlan.TIER_SELF_HOSTED_BUSINESS: "Zulip Business",
|
||||
CustomerPlan.TIER_SELF_HOSTED_COMMUNITY: "Community",
|
||||
# Complimentary access plans should never be billed through Stripe,
|
||||
# so the tier name can exceed the 22 character limit noted above.
|
||||
CustomerPlan.TIER_SELF_HOSTED_LEGACY: "Zulip Basic (complimentary)",
|
||||
}[tier]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.name_from_tier(self.tier)
|
||||
|
||||
def get_plan_status_as_text(self) -> str:
|
||||
return {
|
||||
self.ACTIVE: "Active",
|
||||
self.DOWNGRADE_AT_END_OF_CYCLE: "Downgrade end of cycle",
|
||||
self.FREE_TRIAL: "Free trial",
|
||||
self.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE: "Scheduled switch to annual",
|
||||
self.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE: "Scheduled switch to monthly",
|
||||
self.DOWNGRADE_AT_END_OF_FREE_TRIAL: "Downgrade end of free trial",
|
||||
self.SWITCH_PLAN_TIER_AT_PLAN_END: "New plan scheduled",
|
||||
self.ENDED: "Ended",
|
||||
self.NEVER_STARTED: "Never started",
|
||||
}[self.status]
|
||||
|
||||
def licenses(self) -> int:
|
||||
ledger_entry = LicenseLedger.objects.filter(plan=self).order_by("id").last()
|
||||
assert ledger_entry is not None
|
||||
return ledger_entry.licenses
|
||||
|
||||
def licenses_at_next_renewal(self) -> int | None:
|
||||
if self.status in (
|
||||
CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE,
|
||||
CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL,
|
||||
):
|
||||
return None
|
||||
ledger_entry = LicenseLedger.objects.filter(plan=self).order_by("id").last()
|
||||
assert ledger_entry is not None
|
||||
return ledger_entry.licenses_at_next_renewal
|
||||
|
||||
def is_free_trial(self) -> bool:
|
||||
return self.status == CustomerPlan.FREE_TRIAL
|
||||
|
||||
def is_complimentary_access_plan(self) -> bool:
|
||||
return self.tier in self.COMPLIMENTARY_PLAN_TIERS
|
||||
|
||||
def is_a_paid_plan(self) -> bool:
|
||||
return self.tier in self.PAID_PLAN_TIERS
|
||||
|
||||
|
||||
def get_current_plan_by_customer(customer: Customer) -> CustomerPlan | None:
|
||||
return CustomerPlan.objects.filter(
|
||||
customer=customer, status__lt=CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
).first()
|
||||
|
||||
|
||||
def get_current_plan_by_realm(realm: Realm) -> CustomerPlan | None:
|
||||
customer = get_customer_by_realm(realm)
|
||||
if customer is None:
|
||||
return None
|
||||
return get_current_plan_by_customer(customer)
|
||||
|
||||
|
||||
class LicenseLedger(models.Model):
|
||||
"""
|
||||
This table's purpose is to store the current, and historical,
|
||||
count of "seats" purchased by the organization.
|
||||
|
||||
Because we want to keep historical data, when the purchased
|
||||
seat count changes, a new LicenseLedger object is created,
|
||||
instead of updating the old one. This lets us preserve
|
||||
the entire history of how the seat count changes, which is
|
||||
important for analytics as well as auditing and debugging
|
||||
in case of issues.
|
||||
"""
|
||||
|
||||
plan = models.ForeignKey(CustomerPlan, on_delete=CASCADE)
|
||||
|
||||
# Also True for the initial upgrade.
|
||||
is_renewal = models.BooleanField(default=False)
|
||||
|
||||
event_time = models.DateTimeField()
|
||||
|
||||
# The number of licenses ("seats") purchased by the organization at the time of ledger
|
||||
# entry creation. Normally, to add a user the organization needs at least one spare license.
|
||||
# Once a license is purchased, it is valid till the end of the billing period, irrespective
|
||||
# of whether the license is used or not. So the value of licenses will never decrease for
|
||||
# subsequent LicenseLedger entries in the same billing period.
|
||||
licenses = models.IntegerField()
|
||||
|
||||
# The number of licenses the organization needs in the next billing cycle. The value of
|
||||
# licenses_at_next_renewal can increase or decrease for subsequent LicenseLedger entries in
|
||||
# the same billing period. For plans on automatic license management this value is usually
|
||||
# equal to the number of activated users in the organization.
|
||||
licenses_at_next_renewal = models.IntegerField(null=True)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
ledger_type = "renewal" if self.is_renewal else "update"
|
||||
ledger_time = self.event_time.strftime("%Y-%m-%d %H:%M")
|
||||
return f"License {ledger_type}, {self.licenses} purchased, {self.licenses_at_next_renewal} next cycle, {ledger_time} (id={self.id})"
|
||||
|
||||
|
||||
class SponsoredPlanTypes(Enum):
|
||||
# unspecified used for cloud sponsorship requests
|
||||
UNSPECIFIED = ""
|
||||
COMMUNITY = "Community"
|
||||
BASIC = "Basic"
|
||||
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)
|
||||
|
||||
org_type = models.PositiveSmallIntegerField(
|
||||
default=Realm.ORG_TYPES["unspecified"]["id"],
|
||||
choices=[(t["id"], t["name"]) for t in Realm.ORG_TYPES.values()],
|
||||
)
|
||||
|
||||
MAX_ORG_URL_LENGTH: int = 200
|
||||
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="")
|
||||
plan_to_use_zulip = 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,
|
||||
)
|
@@ -1,11 +0,0 @@
|
||||
from corporate.models.customers import Customer as Customer
|
||||
from corporate.models.licenses import LicenseLedger as LicenseLedger
|
||||
from corporate.models.plans import AbstractCustomerPlan as AbstractCustomerPlan
|
||||
from corporate.models.plans import CustomerPlan as CustomerPlan
|
||||
from corporate.models.plans import CustomerPlanOffer as CustomerPlanOffer
|
||||
from corporate.models.sponsorships import SponsoredPlanTypes as SponsoredPlanTypes
|
||||
from corporate.models.sponsorships import ZulipSponsorshipRequest as ZulipSponsorshipRequest
|
||||
from corporate.models.stripe_state import Event as Event
|
||||
from corporate.models.stripe_state import Invoice as Invoice
|
||||
from corporate.models.stripe_state import PaymentIntent as PaymentIntent
|
||||
from corporate.models.stripe_state import Session as Session
|
@@ -1,89 +0,0 @@
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE, Q
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.models import Realm
|
||||
from zilencer.models import RemoteRealm, RemoteZulipServer
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
"""
|
||||
This model primarily serves to connect a Realm with
|
||||
the corresponding Stripe customer object for payment purposes
|
||||
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)
|
||||
|
||||
# Discounted price for required_plan_tier in cents.
|
||||
# We treat 0 as no discount. Not using `null` here keeps the
|
||||
# checks simpler and avoids the cases where we forget to
|
||||
# check for both `null` and 0.
|
||||
monthly_discounted_price = models.IntegerField(default=0, null=False)
|
||||
annual_discounted_price = models.IntegerField(default=0, null=False)
|
||||
|
||||
minimum_licenses = models.PositiveIntegerField(null=True)
|
||||
# Used for limiting discounted price or a fixed_price
|
||||
# to be used only for a particular CustomerPlan tier.
|
||||
required_plan_tier = models.SmallIntegerField(null=True)
|
||||
# Some non-profit organizations on manual license management pay
|
||||
# only for their paid employees. We don't prevent these
|
||||
# organizations from adding more users than the number of licenses
|
||||
# they purchased.
|
||||
exempt_from_license_number_check = models.BooleanField(default=False)
|
||||
|
||||
# In cents.
|
||||
flat_discount = models.IntegerField(default=2000)
|
||||
# Number of months left in the flat discount period.
|
||||
flat_discounted_months = models.IntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
# Enforce that at least one of these is set.
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
condition=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})"
|
||||
elif self.remote_realm is not None:
|
||||
return f"{self.remote_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})"
|
||||
|
||||
def get_discounted_price_for_plan(self, plan_tier: int, schedule: int) -> int | None:
|
||||
from corporate.models.plans import CustomerPlan
|
||||
|
||||
if plan_tier != self.required_plan_tier:
|
||||
return None
|
||||
|
||||
if schedule == CustomerPlan.BILLING_SCHEDULE_ANNUAL:
|
||||
return self.annual_discounted_price
|
||||
|
||||
assert schedule == CustomerPlan.BILLING_SCHEDULE_MONTHLY
|
||||
return self.monthly_discounted_price
|
||||
|
||||
|
||||
def get_customer_by_realm(realm: Realm) -> Customer | None:
|
||||
return Customer.objects.filter(realm=realm).first()
|
||||
|
||||
|
||||
def get_customer_by_remote_server(remote_server: RemoteZulipServer) -> Customer | None:
|
||||
return Customer.objects.filter(remote_server=remote_server).first()
|
||||
|
||||
|
||||
def get_customer_by_remote_realm(remote_realm: RemoteRealm) -> Customer | None: # nocoverage
|
||||
return Customer.objects.filter(remote_realm=remote_realm).first()
|
@@ -1,45 +0,0 @@
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
from typing_extensions import override
|
||||
|
||||
from corporate.models.plans import CustomerPlan
|
||||
|
||||
|
||||
class LicenseLedger(models.Model):
|
||||
"""
|
||||
This table's purpose is to store the current, and historical,
|
||||
count of "seats" purchased by the organization.
|
||||
|
||||
Because we want to keep historical data, when the purchased
|
||||
seat count changes, a new LicenseLedger object is created,
|
||||
instead of updating the old one. This lets us preserve
|
||||
the entire history of how the seat count changes, which is
|
||||
important for analytics as well as auditing and debugging
|
||||
in case of issues.
|
||||
"""
|
||||
|
||||
plan = models.ForeignKey(CustomerPlan, on_delete=CASCADE)
|
||||
|
||||
# Also True for the initial upgrade.
|
||||
is_renewal = models.BooleanField(default=False)
|
||||
|
||||
event_time = models.DateTimeField()
|
||||
|
||||
# The number of licenses ("seats") purchased by the organization at the time of ledger
|
||||
# entry creation. Normally, to add a user the organization needs at least one spare license.
|
||||
# Once a license is purchased, it is valid till the end of the billing period, irrespective
|
||||
# of whether the license is used or not. So the value of licenses will never decrease for
|
||||
# subsequent LicenseLedger entries in the same billing period.
|
||||
licenses = models.IntegerField()
|
||||
|
||||
# The number of licenses the organization needs in the next billing cycle. The value of
|
||||
# licenses_at_next_renewal can increase or decrease for subsequent LicenseLedger entries in
|
||||
# the same billing period. For plans on automatic license management this value is usually
|
||||
# equal to the number of activated users in the organization.
|
||||
licenses_at_next_renewal = models.IntegerField(null=True)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
ledger_type = "renewal" if self.is_renewal else "update"
|
||||
ledger_time = self.event_time.strftime("%Y-%m-%d %H:%M")
|
||||
return f"License {ledger_type}, {self.licenses} purchased, {self.licenses_at_next_renewal} next cycle, {ledger_time} (id={self.id})"
|
@@ -1,265 +0,0 @@
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
from typing_extensions import override
|
||||
|
||||
from corporate.models.customers import Customer, get_customer_by_realm
|
||||
from zerver.models import Realm
|
||||
|
||||
|
||||
class AbstractCustomerPlan(models.Model):
|
||||
# A customer can only have one ACTIVE / CONFIGURED plan,
|
||||
# but old, inactive / processed plans are preserved to allow
|
||||
# auditing - so there can be multiple CustomerPlan / CustomerPlanOffer
|
||||
# objects pointing to one Customer.
|
||||
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
||||
|
||||
fixed_price = models.IntegerField(null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class CustomerPlanOffer(AbstractCustomerPlan):
|
||||
"""
|
||||
This is for storing offers configured via /support which
|
||||
the customer is yet to buy or schedule a purchase.
|
||||
|
||||
Once customer buys or schedules a purchase, we create a
|
||||
CustomerPlan record. The record in this table stays for
|
||||
audit purpose with status=PROCESSED.
|
||||
"""
|
||||
|
||||
TIER_CLOUD_STANDARD = 1
|
||||
TIER_CLOUD_PLUS = 2
|
||||
TIER_SELF_HOSTED_BASIC = 103
|
||||
TIER_SELF_HOSTED_BUSINESS = 104
|
||||
tier = models.SmallIntegerField()
|
||||
|
||||
# Whether the offer is:
|
||||
# * only configured
|
||||
# * processed by the customer to buy or schedule a purchase.
|
||||
CONFIGURED = 1
|
||||
PROCESSED = 2
|
||||
status = models.SmallIntegerField()
|
||||
|
||||
# ID of invoice sent when chose to 'Pay by invoice'.
|
||||
sent_invoice_id = models.CharField(max_length=255, null=True)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name} (status: {self.get_plan_status_as_text()})"
|
||||
|
||||
def get_plan_status_as_text(self) -> str:
|
||||
return {
|
||||
self.CONFIGURED: "Configured",
|
||||
self.PROCESSED: "Processed",
|
||||
}[self.status]
|
||||
|
||||
@staticmethod
|
||||
def name_from_tier(tier: int) -> str:
|
||||
return {
|
||||
CustomerPlanOffer.TIER_CLOUD_STANDARD: "Zulip Cloud Standard",
|
||||
CustomerPlanOffer.TIER_CLOUD_PLUS: "Zulip Cloud Plus",
|
||||
CustomerPlanOffer.TIER_SELF_HOSTED_BASIC: "Zulip Basic",
|
||||
CustomerPlanOffer.TIER_SELF_HOSTED_BUSINESS: "Zulip Business",
|
||||
}[tier]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.name_from_tier(self.tier)
|
||||
|
||||
|
||||
class CustomerPlan(AbstractCustomerPlan):
|
||||
"""
|
||||
This is for storing most of the fiddly details
|
||||
of the customer's plan.
|
||||
"""
|
||||
|
||||
automanage_licenses = models.BooleanField(default=False)
|
||||
charge_automatically = models.BooleanField(default=False)
|
||||
|
||||
# Both of the price_per_license and fixed_price are in cents. Exactly
|
||||
# one of them should be set. fixed_price is only for manual deals, and
|
||||
# can't be set via the self-serve billing system.
|
||||
price_per_license = models.IntegerField(null=True)
|
||||
|
||||
# Discount for current `billing_schedule`. For display purposes only.
|
||||
# Explicitly set to be TextField to avoid being used in calculations.
|
||||
# NOTE: This discount can be different for annual and monthly schedules.
|
||||
discount = models.TextField(null=True)
|
||||
|
||||
# Initialized with the time of plan creation. Used for calculating
|
||||
# start of next billing cycle, next invoice date etc. This value
|
||||
# should never be modified. The only exception is when we change
|
||||
# the status of the plan from free trial to active and reset the
|
||||
# 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",
|
||||
}
|
||||
billing_schedule = models.SmallIntegerField()
|
||||
|
||||
# The next date the billing system should go through ledger
|
||||
# entries and create invoices for additional users or plan
|
||||
# renewal. Since we use a daily cron job for invoicing, the
|
||||
# invoice will be generated the first time the cron job runs after
|
||||
# next_invoice_date.
|
||||
next_invoice_date = models.DateTimeField(db_index=True, null=True)
|
||||
|
||||
# Flag to track if an email has been sent to Zulip team for delay
|
||||
# of invoicing by >= one day. Helps to send an email only once
|
||||
# and not every time when cron run.
|
||||
stale_audit_log_data_email_sent = models.BooleanField(default=False)
|
||||
|
||||
# Flag to track if an email has been sent to Zulip team to
|
||||
# review the pricing, 60 days before the end date. Helps to send
|
||||
# an email only once and not every time when cron run.
|
||||
reminder_to_review_plan_email_sent = models.BooleanField(default=False)
|
||||
|
||||
# On next_invoice_date, we call invoice_plan, which goes through
|
||||
# ledger entries that were created after invoiced_through and
|
||||
# process them. An invoice will be generated for any additional
|
||||
# users and/or plan renewal (if it's the end of the billing cycle).
|
||||
# Once all new ledger entries have been processed, invoiced_through
|
||||
# will be have been set to the last ledger entry we checked.
|
||||
invoiced_through = models.ForeignKey(
|
||||
"LicenseLedger", null=True, on_delete=CASCADE, related_name="+"
|
||||
)
|
||||
end_date = models.DateTimeField(null=True)
|
||||
|
||||
INVOICING_STATUS_DONE = 1
|
||||
INVOICING_STATUS_STARTED = 2
|
||||
INVOICING_STATUS_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)
|
||||
|
||||
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_BASIC = 103
|
||||
TIER_SELF_HOSTED_BUSINESS = 104
|
||||
TIER_SELF_HOSTED_ENTERPRISE = 105
|
||||
tier = models.SmallIntegerField()
|
||||
|
||||
PAID_PLAN_TIERS = [
|
||||
TIER_CLOUD_STANDARD,
|
||||
TIER_CLOUD_PLUS,
|
||||
TIER_SELF_HOSTED_BASIC,
|
||||
TIER_SELF_HOSTED_BUSINESS,
|
||||
TIER_SELF_HOSTED_ENTERPRISE,
|
||||
]
|
||||
|
||||
COMPLIMENTARY_PLAN_TIERS = [TIER_SELF_HOSTED_LEGACY]
|
||||
|
||||
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
|
||||
# "Live" plans should have a value < LIVE_STATUS_THRESHOLD.
|
||||
# There should be at most one live plan per customer.
|
||||
LIVE_STATUS_THRESHOLD = 10
|
||||
ENDED = 11
|
||||
NEVER_STARTED = 12
|
||||
status = models.SmallIntegerField(default=ACTIVE)
|
||||
|
||||
# Currently, all the fixed-price plans are configured for one year.
|
||||
# In future, we might change this to a field.
|
||||
FIXED_PRICE_PLAN_DURATION_MONTHS = 12
|
||||
|
||||
# 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_BASIC: "Zulip Basic",
|
||||
CustomerPlan.TIER_SELF_HOSTED_BUSINESS: "Zulip Business",
|
||||
CustomerPlan.TIER_SELF_HOSTED_COMMUNITY: "Community",
|
||||
# Complimentary access plans should never be billed through Stripe,
|
||||
# so the tier name can exceed the 22 character limit noted above.
|
||||
CustomerPlan.TIER_SELF_HOSTED_LEGACY: "Zulip Basic (complimentary)",
|
||||
}[tier]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.name_from_tier(self.tier)
|
||||
|
||||
def get_plan_status_as_text(self) -> str:
|
||||
return {
|
||||
self.ACTIVE: "Active",
|
||||
self.DOWNGRADE_AT_END_OF_CYCLE: "Downgrade end of cycle",
|
||||
self.FREE_TRIAL: "Free trial",
|
||||
self.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE: "Scheduled switch to annual",
|
||||
self.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE: "Scheduled switch to monthly",
|
||||
self.DOWNGRADE_AT_END_OF_FREE_TRIAL: "Downgrade end of free trial",
|
||||
self.SWITCH_PLAN_TIER_AT_PLAN_END: "New plan scheduled",
|
||||
self.ENDED: "Ended",
|
||||
self.NEVER_STARTED: "Never started",
|
||||
}[self.status]
|
||||
|
||||
def licenses(self) -> int:
|
||||
from corporate.models.licenses import LicenseLedger
|
||||
|
||||
ledger_entry = LicenseLedger.objects.filter(plan=self).order_by("id").last()
|
||||
assert ledger_entry is not None
|
||||
return ledger_entry.licenses
|
||||
|
||||
def licenses_at_next_renewal(self) -> int | None:
|
||||
from corporate.models.licenses import LicenseLedger
|
||||
|
||||
if self.status in (
|
||||
CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE,
|
||||
CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL,
|
||||
):
|
||||
return None
|
||||
ledger_entry = LicenseLedger.objects.filter(plan=self).order_by("id").last()
|
||||
assert ledger_entry is not None
|
||||
return ledger_entry.licenses_at_next_renewal
|
||||
|
||||
def is_free_trial(self) -> bool:
|
||||
return self.status == CustomerPlan.FREE_TRIAL
|
||||
|
||||
def is_complimentary_access_plan(self) -> bool:
|
||||
return self.tier in self.COMPLIMENTARY_PLAN_TIERS
|
||||
|
||||
def is_a_paid_plan(self) -> bool:
|
||||
return self.tier in self.PAID_PLAN_TIERS
|
||||
|
||||
|
||||
def get_current_plan_by_customer(customer: Customer) -> CustomerPlan | None:
|
||||
return CustomerPlan.objects.filter(
|
||||
customer=customer, status__lt=CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
).first()
|
||||
|
||||
|
||||
def get_current_plan_by_realm(realm: Realm) -> CustomerPlan | None:
|
||||
customer = get_customer_by_realm(realm)
|
||||
if customer is None:
|
||||
return None
|
||||
return get_current_plan_by_customer(customer)
|
@@ -1,40 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
|
||||
from corporate.models.customers import Customer
|
||||
from zerver.models import Realm, UserProfile
|
||||
|
||||
|
||||
class SponsoredPlanTypes(Enum):
|
||||
# unspecified used for cloud sponsorship requests
|
||||
UNSPECIFIED = ""
|
||||
COMMUNITY = "Community"
|
||||
BASIC = "Basic"
|
||||
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)
|
||||
|
||||
org_type = models.PositiveSmallIntegerField(
|
||||
default=Realm.ORG_TYPES["unspecified"]["id"],
|
||||
choices=[(t["id"], t["name"]) for t in Realm.ORG_TYPES.values()],
|
||||
)
|
||||
|
||||
MAX_ORG_URL_LENGTH: int = 200
|
||||
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="")
|
||||
plan_to_use_zulip = 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,
|
||||
)
|
@@ -1,176 +0,0 @@
|
||||
from typing import Any, 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, SET_NULL
|
||||
|
||||
from corporate.models.customers import Customer
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
stripe_event_id = models.CharField(max_length=255)
|
||||
|
||||
type = models.CharField(max_length=255)
|
||||
|
||||
RECEIVED = 1
|
||||
EVENT_HANDLER_STARTED = 30
|
||||
EVENT_HANDLER_FAILED = 40
|
||||
EVENT_HANDLER_SUCCEEDED = 50
|
||||
status = models.SmallIntegerField(default=RECEIVED)
|
||||
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField(db_index=True)
|
||||
content_object = GenericForeignKey("content_type", "object_id")
|
||||
|
||||
handler_error = models.JSONField(default=None, null=True)
|
||||
|
||||
def get_event_handler_details_as_dict(self) -> dict[str, Any]:
|
||||
details_dict = {}
|
||||
details_dict["status"] = {
|
||||
Event.RECEIVED: "not_started",
|
||||
Event.EVENT_HANDLER_STARTED: "started",
|
||||
Event.EVENT_HANDLER_FAILED: "failed",
|
||||
Event.EVENT_HANDLER_SUCCEEDED: "succeeded",
|
||||
}[self.status]
|
||||
if self.handler_error:
|
||||
details_dict["error"] = self.handler_error
|
||||
return details_dict
|
||||
|
||||
|
||||
def get_last_associated_event_by_type(
|
||||
content_object: Union["Invoice", "PaymentIntent", "Session"], event_type: str
|
||||
) -> Event | None:
|
||||
content_type = ContentType.objects.get_for_model(type(content_object))
|
||||
return Event.objects.filter(
|
||||
content_type=content_type, object_id=content_object.id, type=event_type
|
||||
).last()
|
||||
|
||||
|
||||
class Session(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
||||
stripe_session_id = models.CharField(max_length=255, unique=True)
|
||||
|
||||
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)
|
||||
|
||||
# CustomerPlan tier that the user is upgrading to.
|
||||
tier = models.SmallIntegerField(null=True)
|
||||
|
||||
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.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]:
|
||||
session_dict: dict[str, Any] = {}
|
||||
|
||||
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
|
||||
)
|
||||
session_dict["tier"] = self.tier
|
||||
event = self.get_last_associated_event()
|
||||
if event is not None:
|
||||
session_dict["event_handler"] = event.get_event_handler_details_as_dict()
|
||||
return session_dict
|
||||
|
||||
def get_last_associated_event(self) -> Event | None:
|
||||
if self.status == Session.CREATED:
|
||||
return None
|
||||
return get_last_associated_event_by_type(self, "checkout.session.completed")
|
||||
|
||||
|
||||
class PaymentIntent(models.Model): # nocoverage
|
||||
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
||||
stripe_payment_intent_id = models.CharField(max_length=255, unique=True)
|
||||
|
||||
REQUIRES_PAYMENT_METHOD = 1
|
||||
REQUIRES_CONFIRMATION = 20
|
||||
REQUIRES_ACTION = 30
|
||||
PROCESSING = 40
|
||||
REQUIRES_CAPTURE = 50
|
||||
CANCELLED = 60
|
||||
SUCCEEDED = 70
|
||||
|
||||
status = models.SmallIntegerField()
|
||||
last_payment_error = models.JSONField(default=None, null=True)
|
||||
|
||||
@classmethod
|
||||
def get_status_integer_from_status_text(cls, status_text: str) -> int:
|
||||
return getattr(cls, status_text.upper())
|
||||
|
||||
def get_status_as_string(self) -> str:
|
||||
return {
|
||||
PaymentIntent.REQUIRES_PAYMENT_METHOD: "requires_payment_method",
|
||||
PaymentIntent.REQUIRES_CONFIRMATION: "requires_confirmation",
|
||||
PaymentIntent.REQUIRES_ACTION: "requires_action",
|
||||
PaymentIntent.PROCESSING: "processing",
|
||||
PaymentIntent.REQUIRES_CAPTURE: "requires_capture",
|
||||
PaymentIntent.CANCELLED: "cancelled",
|
||||
PaymentIntent.SUCCEEDED: "succeeded",
|
||||
}[self.status]
|
||||
|
||||
def get_last_associated_event(self) -> Event | None:
|
||||
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
|
||||
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 event is not None:
|
||||
payment_intent_dict["event_handler"] = event.get_event_handler_details_as_dict()
|
||||
return payment_intent_dict
|
||||
|
||||
|
||||
class Invoice(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
||||
stripe_invoice_id = models.CharField(max_length=255, unique=True)
|
||||
plan = models.ForeignKey("CustomerPlan", null=True, default=None, on_delete=SET_NULL)
|
||||
is_created_for_free_trial_upgrade = models.BooleanField(default=False)
|
||||
|
||||
SENT = 1
|
||||
PAID = 2
|
||||
VOID = 3
|
||||
status = models.SmallIntegerField()
|
||||
|
||||
def get_status_as_string(self) -> str:
|
||||
return {
|
||||
Invoice.SENT: "sent",
|
||||
Invoice.PAID: "paid",
|
||||
Invoice.VOID: "void",
|
||||
}[self.status]
|
||||
|
||||
def get_last_associated_event(self) -> Event | None:
|
||||
if self.status == Invoice.PAID:
|
||||
event_type = "invoice.paid"
|
||||
# TODO: Add test for this case. Not sure how to trigger naturally.
|
||||
else: # nocoverage
|
||||
return None # nocoverage
|
||||
return get_last_associated_event_by_type(self, event_type)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
stripe_invoice_dict: dict[str, Any] = {}
|
||||
stripe_invoice_dict["status"] = self.get_status_as_string()
|
||||
event = self.get_last_associated_event()
|
||||
if event is not None:
|
||||
stripe_invoice_dict["event_handler"] = event.get_event_handler_details_as_dict()
|
||||
return stripe_invoice_dict
|
@@ -19,8 +19,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"captured": true,
|
||||
@@ -36,15 +35,13 @@
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
"id": "add_minimum_licenses--Charge.list.1.json",
|
||||
"invoice": "in_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "charge",
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"advice_code": null,
|
||||
"network_advice_code": null,
|
||||
"network_decline_code": null,
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
@@ -86,12 +83,10 @@
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"network_transaction_id": "100110997670110",
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 100000,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
@@ -102,6 +97,13 @@
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/invoices/NORMALIZED?s=ap",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/add_minimum_licenses--Charge.list.1.json/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"api_version": "2025-04-30.basil",
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
@@ -9,6 +9,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,30 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"api_version": "2025-04-30.basil",
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 100000,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 100000,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -42,6 +41,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -60,6 +60,7 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 100000,
|
||||
"amount_excluding_tax": 100000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
@@ -67,36 +68,46 @@
|
||||
"discounts": [],
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 25,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "4000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -118,7 +129,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -128,6 +141,7 @@
|
||||
"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,
|
||||
@@ -137,6 +151,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -148,53 +163,57 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 100000,
|
||||
"subtotal_excluding_tax": 100000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 100000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 100000,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "add_minimum_licenses--Event.list.3.json",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"pending_webhooks": 2,
|
||||
"request": {
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"id": "add_minimum_licenses--Event.list.3.json",
|
||||
"idempotency_key": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
"type": "invoice.payment_succeeded"
|
||||
},
|
||||
{
|
||||
"api_version": "2025-04-30.basil",
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 100000,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 100000,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -211,6 +230,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -229,6 +249,7 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 100000,
|
||||
"amount_excluding_tax": 100000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
@@ -236,36 +257,46 @@
|
||||
"discounts": [],
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 25,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "4000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -287,7 +318,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -297,6 +330,7 @@
|
||||
"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,
|
||||
@@ -306,6 +340,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -317,15 +352,20 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 100000,
|
||||
"subtotal_excluding_tax": 100000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 100000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 100000,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "add_minimum_licenses--Event.list.3.json",
|
||||
@@ -333,10 +373,211 @@
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"id": "add_minimum_licenses--Event.list.3.json",
|
||||
"idempotency_key": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
"type": "invoice.paid"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 100000,
|
||||
"amount_paid": 100000,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_fields": null,
|
||||
"customer": "cus_NORMALIZED",
|
||||
"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_NORMALIZED/test_NORMALIZED?s=ap",
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED/test_NORMALIZED/pdf?s=ap",
|
||||
"issuer": {
|
||||
"type": "self"
|
||||
},
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [
|
||||
{
|
||||
"amount": 100000,
|
||||
"amount_excluding_tax": 100000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"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": "add_minimum_licenses--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 25,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "4000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 1,
|
||||
"url": "/v1/invoices/add_minimum_licenses--Event.list.2.json/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_schedule": "1",
|
||||
"current_plan_id": "None",
|
||||
"license_management": "automatic",
|
||||
"licenses": "25",
|
||||
"on_free_trial": "False",
|
||||
"plan_tier": "1",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"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"
|
||||
},
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"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": 100000,
|
||||
"subtotal_excluding_tax": 100000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 100000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 100000,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
},
|
||||
"previous_attributes": {
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 100000,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"charge": null,
|
||||
"paid": false,
|
||||
"status": "open",
|
||||
"status_transitions": {
|
||||
"paid_at": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "add_minimum_licenses--Event.list.3.json",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "add_minimum_licenses--Event.list.3.json",
|
||||
"idempotency_key": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
"type": "invoice.updated"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
|
@@ -1,25 +1,24 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_overpaid": 0,
|
||||
"amount_due": 100000,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_remaining": 100000,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -36,6 +35,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": null,
|
||||
@@ -51,10 +51,62 @@
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [],
|
||||
"data": [
|
||||
{
|
||||
"amount": 100000,
|
||||
"amount_excluding_tax": 100000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"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": "add_minimum_licenses--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 25,
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "4000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"total_count": 1,
|
||||
"url": "/v1/invoices/add_minimum_licenses--Event.list.2.json/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
@@ -71,7 +123,9 @@
|
||||
"number": null,
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -81,6 +135,7 @@
|
||||
"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,
|
||||
@@ -90,6 +145,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -101,13 +157,18 @@
|
||||
"paid_at": null,
|
||||
"voided_at": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 100000,
|
||||
"subtotal_excluding_tax": 100000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total": 100000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_excluding_tax": 100000,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
|
@@ -1,25 +1,24 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 100000,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 100000,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -36,6 +35,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -54,6 +54,7 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 100000,
|
||||
"amount_excluding_tax": 100000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
@@ -61,36 +62,46 @@
|
||||
"discounts": [],
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 25,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "4000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -112,7 +123,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -122,6 +135,7 @@
|
||||
"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,
|
||||
@@ -131,6 +145,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -142,13 +157,18 @@
|
||||
"paid_at": null,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 100000,
|
||||
"subtotal_excluding_tax": 100000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 100000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 100000,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
|
@@ -1,25 +1,24 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 100000,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 100000,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -36,6 +35,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -54,6 +54,7 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 100000,
|
||||
"amount_excluding_tax": 100000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
@@ -61,36 +62,46 @@
|
||||
"discounts": [],
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 25,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "4000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -112,7 +123,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -122,6 +135,7 @@
|
||||
"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,
|
||||
@@ -131,6 +145,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -142,13 +157,18 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 100000,
|
||||
"subtotal_excluding_tax": 100000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 100000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 100000,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
|
@@ -7,25 +7,41 @@
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"invoice": "in_NORMALIZED",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "invoiceitem",
|
||||
"parent": null,
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "add_minimum_licenses--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 25,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"test_clock": null
|
||||
"test_clock": null,
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
}
|
||||
|
@@ -11,8 +11,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -35,7 +34,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"adaptive_pricing": null,
|
||||
"after_expiration": null,
|
||||
"allow_promotion_codes": null,
|
||||
"amount_subtotal": null,
|
||||
@@ -7,16 +6,12 @@
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"billing_address_collection": "required",
|
||||
"cancel_url": "http://zulip.testserver/upgrade/?manual_license_management=false&tier=1",
|
||||
"client_reference_id": null,
|
||||
"client_secret": null,
|
||||
"collected_information": {
|
||||
"shipping_details": null
|
||||
},
|
||||
"consent": null,
|
||||
"consent_collection": null,
|
||||
"created": 1000000000,
|
||||
@@ -40,7 +35,6 @@
|
||||
"tax_ids": null
|
||||
},
|
||||
"customer_email": null,
|
||||
"discounts": null,
|
||||
"expires_at": 1000000000,
|
||||
"id": "add_minimum_licenses--checkout.Session.create.1.json",
|
||||
"invoice": null,
|
||||
@@ -66,22 +60,21 @@
|
||||
"card"
|
||||
],
|
||||
"payment_status": "no_payment_required",
|
||||
"permissions": null,
|
||||
"phone_number_collection": {
|
||||
"enabled": false
|
||||
},
|
||||
"recovered_from": null,
|
||||
"saved_payment_method_options": null,
|
||||
"setup_intent": "seti_NORMALIZED",
|
||||
"shipping": null,
|
||||
"shipping_address_collection": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_options": [],
|
||||
"shipping_rate": null,
|
||||
"status": "open",
|
||||
"submit_type": null,
|
||||
"subscription": null,
|
||||
"success_url": "http://zulip.testserver/billing/event_status/?stripe_session_id={CHECKOUT_SESSION_ID}",
|
||||
"total_details": null,
|
||||
"ui_mode": "hosted",
|
||||
"url": "https://checkout.stripe.com/c/pay/cs_test_NORMALIZED",
|
||||
"wallet_options": null
|
||||
"url": "https://checkout.stripe.com/c/pay/cs_test_NORMALIZED"
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"adaptive_pricing": null,
|
||||
"after_expiration": null,
|
||||
"allow_promotion_codes": null,
|
||||
"amount_subtotal": null,
|
||||
@@ -9,16 +8,12 @@
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"billing_address_collection": "required",
|
||||
"cancel_url": "http://zulip.testserver/upgrade/?manual_license_management=false&tier=1",
|
||||
"client_reference_id": null,
|
||||
"client_secret": null,
|
||||
"collected_information": {
|
||||
"shipping_details": null
|
||||
},
|
||||
"consent": null,
|
||||
"consent_collection": null,
|
||||
"created": 1000000000,
|
||||
@@ -42,7 +37,6 @@
|
||||
"tax_ids": null
|
||||
},
|
||||
"customer_email": null,
|
||||
"discounts": null,
|
||||
"expires_at": 1000000000,
|
||||
"id": "add_minimum_licenses--checkout.Session.create.1.json",
|
||||
"invoice": null,
|
||||
@@ -68,24 +62,23 @@
|
||||
"card"
|
||||
],
|
||||
"payment_status": "no_payment_required",
|
||||
"permissions": null,
|
||||
"phone_number_collection": {
|
||||
"enabled": false
|
||||
},
|
||||
"recovered_from": null,
|
||||
"saved_payment_method_options": null,
|
||||
"setup_intent": "seti_NORMALIZED",
|
||||
"shipping": null,
|
||||
"shipping_address_collection": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_options": [],
|
||||
"shipping_rate": null,
|
||||
"status": "open",
|
||||
"submit_type": null,
|
||||
"subscription": null,
|
||||
"success_url": "http://zulip.testserver/billing/event_status/?stripe_session_id={CHECKOUT_SESSION_ID}",
|
||||
"total_details": null,
|
||||
"ui_mode": "hosted",
|
||||
"url": "https://checkout.stripe.com/c/pay/cs_test_NORMALIZED",
|
||||
"wallet_options": null
|
||||
"url": "https://checkout.stripe.com/c/pay/cs_test_NORMALIZED"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
|
@@ -19,8 +19,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"captured": true,
|
||||
@@ -36,15 +35,13 @@
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
"id": "attach_discount_to_realm--Charge.list.1.json",
|
||||
"invoice": "in_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "charge",
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"advice_code": null,
|
||||
"network_advice_code": null,
|
||||
"network_decline_code": null,
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
@@ -86,22 +83,26 @@
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"network_transaction_id": "100110997670110",
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 7200,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"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/invoices/NORMALIZED?s=ap",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/attach_discount_to_realm--Charge.list.1.json/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
|
@@ -19,8 +19,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"captured": true,
|
||||
@@ -36,15 +35,13 @@
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
"id": "attach_discount_to_realm--Charge.list.2.json",
|
||||
"invoice": "in_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "charge",
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"advice_code": null,
|
||||
"network_advice_code": null,
|
||||
"network_decline_code": null,
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
@@ -86,12 +83,10 @@
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"network_transaction_id": "100110997670110",
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 36000,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure": null,
|
||||
"wallet": null
|
||||
},
|
||||
@@ -102,6 +97,13 @@
|
||||
"receipt_number": null,
|
||||
"receipt_url": "https://pay.stripe.com/receipts/invoices/NORMALIZED?s=ap",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/attach_discount_to_realm--Charge.list.2.json/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
@@ -131,8 +133,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"calculated_statement_descriptor": "ZULIP CLOUD STANDARD",
|
||||
"captured": true,
|
||||
@@ -148,15 +149,13 @@
|
||||
"failure_message": null,
|
||||
"fraud_details": {},
|
||||
"id": "attach_discount_to_realm--Charge.list.1.json",
|
||||
"invoice": "in_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "charge",
|
||||
"on_behalf_of": null,
|
||||
"order": null,
|
||||
"outcome": {
|
||||
"advice_code": null,
|
||||
"network_advice_code": null,
|
||||
"network_decline_code": null,
|
||||
"network_status": "approved_by_network",
|
||||
"reason": null,
|
||||
"risk_level": "normal",
|
||||
@@ -198,22 +197,26 @@
|
||||
"network_token": {
|
||||
"used": false
|
||||
},
|
||||
"network_transaction_id": "100110997670110",
|
||||
"overcapture": {
|
||||
"maximum_amount_capturable": 7200,
|
||||
"status": "unavailable"
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"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/invoices/NORMALIZED?s=ap",
|
||||
"refunded": false,
|
||||
"refunds": {
|
||||
"data": [],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"url": "/v1/charges/attach_discount_to_realm--Charge.list.1.json/refunds"
|
||||
},
|
||||
"review": null,
|
||||
"shipping": null,
|
||||
"source": null,
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
@@ -25,8 +26,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"api_version": "2025-04-30.basil",
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
@@ -9,6 +9,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": null,
|
||||
"default_currency": null,
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,30 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"api_version": "2025-04-30.basil",
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 7200,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 7200,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -42,6 +41,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -60,6 +60,7 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
@@ -67,36 +68,46 @@
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"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,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -118,7 +129,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -128,6 +141,7 @@
|
||||
"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,
|
||||
@@ -137,6 +151,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -148,21 +163,26 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 7200,
|
||||
"subtotal_excluding_tax": 7200,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 7200,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 7200,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "attach_discount_to_realm--Event.list.3.json",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"pending_webhooks": 2,
|
||||
"request": {
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"idempotency_key": "00000000-0000-0000-0000-000000000000"
|
||||
@@ -170,31 +190,30 @@
|
||||
"type": "invoice.payment_succeeded"
|
||||
},
|
||||
{
|
||||
"api_version": "2025-04-30.basil",
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 7200,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 7200,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -211,6 +230,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -229,6 +249,7 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
@@ -236,36 +257,46 @@
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"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,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -287,7 +318,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -297,6 +330,7 @@
|
||||
"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,
|
||||
@@ -306,6 +340,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -317,26 +352,232 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 7200,
|
||||
"subtotal_excluding_tax": 7200,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 7200,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 7200,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
},
|
||||
"id": "attach_discount_to_realm--Event.list.3.json",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"pending_webhooks": 1,
|
||||
"request": {
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"idempotency_key": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
"type": "invoice.paid"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 7200,
|
||||
"amount_paid": 7200,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_fields": null,
|
||||
"customer": "cus_NORMALIZED",
|
||||
"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_NORMALIZED/test_NORMALIZED?s=ap",
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED/test_NORMALIZED/pdf?s=ap",
|
||||
"issuer": {
|
||||
"type": "self"
|
||||
},
|
||||
"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": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"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": "attach_discount_to_realm--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 1,
|
||||
"url": "/v1/invoices/attach_discount_to_realm--Event.list.2.json/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_schedule": "1",
|
||||
"current_plan_id": "None",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"on_free_trial": "False",
|
||||
"plan_tier": "1",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"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"
|
||||
},
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"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": 7200,
|
||||
"subtotal_excluding_tax": 7200,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 7200,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 7200,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
},
|
||||
"previous_attributes": {
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 7200,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"charge": null,
|
||||
"paid": false,
|
||||
"status": "open",
|
||||
"status_transitions": {
|
||||
"paid_at": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "attach_discount_to_realm--Event.list.3.json",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 1,
|
||||
"request": {
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"idempotency_key": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
"type": "invoice.updated"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"api_version": "2025-04-30.basil",
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
@@ -9,6 +9,7 @@
|
||||
"balance": 0,
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"default_currency": "usd",
|
||||
"default_source": null,
|
||||
"delinquent": false,
|
||||
"description": "zulip (Zulip Dev)",
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,30 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"api_version": "2025-04-30.basil",
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 36000,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 36000,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -42,6 +41,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -60,43 +60,54 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"id": "attach_discount_to_realm--Event.list.7.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.6.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 6000,
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -118,7 +129,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -128,6 +141,7 @@
|
||||
"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,
|
||||
@@ -137,6 +151,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -148,14 +163,19 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 36000,
|
||||
"subtotal_excluding_tax": 36000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 36000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 36000,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
}
|
||||
},
|
||||
@@ -168,6 +188,396 @@
|
||||
"idempotency_key": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
"type": "invoice.payment_succeeded"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 36000,
|
||||
"amount_paid": 36000,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_fields": null,
|
||||
"customer": "cus_NORMALIZED",
|
||||
"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_NORMALIZED/test_NORMALIZED?s=ap",
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED/test_NORMALIZED/pdf?s=ap",
|
||||
"issuer": {
|
||||
"type": "self"
|
||||
},
|
||||
"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": "attach_discount_to_realm--Event.list.7.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.6.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"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": "attach_discount_to_realm--Event.list.6.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 6000,
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 1,
|
||||
"url": "/v1/invoices/attach_discount_to_realm--Event.list.6.json/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_schedule": "1",
|
||||
"current_plan_id": "None",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"on_free_trial": "False",
|
||||
"plan_tier": "1",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"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"
|
||||
},
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"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": 36000,
|
||||
"subtotal_excluding_tax": 36000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 36000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 36000,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
}
|
||||
},
|
||||
"id": "attach_discount_to_realm--Event.list.7.json",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"idempotency_key": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
"type": "invoice.paid"
|
||||
},
|
||||
{
|
||||
"api_version": "2020-08-27",
|
||||
"created": 1000000000,
|
||||
"data": {
|
||||
"object": {
|
||||
"account_country": "US",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 36000,
|
||||
"amount_paid": 36000,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_fields": null,
|
||||
"customer": "cus_NORMALIZED",
|
||||
"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_NORMALIZED/test_NORMALIZED?s=ap",
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED/test_NORMALIZED/pdf?s=ap",
|
||||
"issuer": {
|
||||
"type": "self"
|
||||
},
|
||||
"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": "attach_discount_to_realm--Event.list.7.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.6.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"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": "attach_discount_to_realm--Event.list.6.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 6000,
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 1,
|
||||
"url": "/v1/invoices/attach_discount_to_realm--Event.list.6.json/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
"billing_schedule": "1",
|
||||
"current_plan_id": "None",
|
||||
"license_management": "automatic",
|
||||
"licenses": "6",
|
||||
"on_free_trial": "False",
|
||||
"plan_tier": "1",
|
||||
"user_id": "10"
|
||||
},
|
||||
"next_payment_attempt": null,
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"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"
|
||||
},
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"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": 36000,
|
||||
"subtotal_excluding_tax": 36000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 36000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 36000,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
},
|
||||
"previous_attributes": {
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 36000,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"charge": null,
|
||||
"paid": false,
|
||||
"status": "open",
|
||||
"status_transitions": {
|
||||
"paid_at": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "attach_discount_to_realm--Event.list.7.json",
|
||||
"livemode": false,
|
||||
"object": "event",
|
||||
"pending_webhooks": 0,
|
||||
"request": {
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"idempotency_key": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
"type": "invoice.updated"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
|
@@ -1,25 +1,24 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_overpaid": 0,
|
||||
"amount_due": 7200,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_remaining": 7200,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -36,6 +35,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": null,
|
||||
@@ -51,10 +51,62 @@
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [],
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"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": "attach_discount_to_realm--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"total_count": 1,
|
||||
"url": "/v1/invoices/attach_discount_to_realm--Event.list.2.json/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
@@ -71,7 +123,9 @@
|
||||
"number": null,
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -81,6 +135,7 @@
|
||||
"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,
|
||||
@@ -90,6 +145,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -101,13 +157,18 @@
|
||||
"paid_at": null,
|
||||
"voided_at": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 7200,
|
||||
"subtotal_excluding_tax": 7200,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total": 7200,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_excluding_tax": 7200,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
|
@@ -1,25 +1,24 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_overpaid": 0,
|
||||
"amount_due": 36000,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_remaining": 36000,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -36,6 +35,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": null,
|
||||
@@ -51,10 +51,62 @@
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [],
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.7.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.6.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"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": "attach_discount_to_realm--Event.list.6.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 6000,
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"total_count": 1,
|
||||
"url": "/v1/invoices/attach_discount_to_realm--Event.list.6.json/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
@@ -71,7 +123,9 @@
|
||||
"number": null,
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -81,6 +135,7 @@
|
||||
"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,
|
||||
@@ -90,6 +145,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -101,13 +157,18 @@
|
||||
"paid_at": null,
|
||||
"voided_at": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 36000,
|
||||
"subtotal_excluding_tax": 36000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total": 36000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_excluding_tax": 36000,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
|
@@ -1,25 +1,24 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 0,
|
||||
"amount_overpaid": 0,
|
||||
"amount_due": 24000,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 0,
|
||||
"amount_remaining": 24000,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"auto_advance": true,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": 1000000000,
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -36,6 +35,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": null,
|
||||
@@ -51,10 +51,62 @@
|
||||
"last_finalization_error": null,
|
||||
"latest_revision": null,
|
||||
"lines": {
|
||||
"data": [],
|
||||
"data": [
|
||||
{
|
||||
"amount": 24000,
|
||||
"amount_excluding_tax": 24000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard - renewal",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Invoice.create.3.json",
|
||||
"invoice": "attach_discount_to_realm--Invoice.create.3.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"period": {
|
||||
"end": 1388631845,
|
||||
"start": 1357095845
|
||||
},
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Invoice.create.3.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 4000,
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"object": "list",
|
||||
"total_count": 0,
|
||||
"total_count": 1,
|
||||
"url": "/v1/invoices/attach_discount_to_realm--Invoice.create.3.json/lines"
|
||||
},
|
||||
"livemode": false,
|
||||
@@ -63,7 +115,9 @@
|
||||
"number": null,
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": null,
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -73,6 +127,7 @@
|
||||
"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,
|
||||
@@ -82,6 +137,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -93,13 +149,18 @@
|
||||
"paid_at": null,
|
||||
"voided_at": null
|
||||
},
|
||||
"subtotal": 0,
|
||||
"subtotal_excluding_tax": 0,
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 24000,
|
||||
"subtotal_excluding_tax": 24000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 0,
|
||||
"total": 24000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 0,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_excluding_tax": 24000,
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
|
@@ -1,25 +1,24 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 7200,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 7200,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -36,6 +35,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -54,6 +54,7 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
@@ -61,36 +62,46 @@
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"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,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -112,7 +123,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -122,6 +135,7 @@
|
||||
"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,
|
||||
@@ -131,6 +145,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -142,13 +157,18 @@
|
||||
"paid_at": null,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 7200,
|
||||
"subtotal_excluding_tax": 7200,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 7200,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 7200,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
|
@@ -1,25 +1,24 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 36000,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 36000,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -36,6 +35,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -54,43 +54,54 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"id": "attach_discount_to_realm--Event.list.7.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.6.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 6000,
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -112,7 +123,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -122,6 +135,7 @@
|
||||
"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,
|
||||
@@ -131,6 +145,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -142,13 +157,18 @@
|
||||
"paid_at": null,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 36000,
|
||||
"subtotal_excluding_tax": 36000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 36000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 36000,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
|
@@ -1,25 +1,24 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 24000,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 24000,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"auto_advance": true,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -36,6 +35,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -54,43 +54,54 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 24000,
|
||||
"amount_excluding_tax": 24000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard - renewal",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Invoice.finalize_invoice.3.json",
|
||||
"id": "attach_discount_to_realm--Invoice.create.3.json",
|
||||
"invoice": "attach_discount_to_realm--Invoice.create.3.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1388631845,
|
||||
"start": 1357095845
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Invoice.create.3.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "4000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -104,7 +115,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -114,6 +127,7 @@
|
||||
"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,
|
||||
@@ -123,6 +137,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -134,13 +149,18 @@
|
||||
"paid_at": null,
|
||||
"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_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
|
@@ -2,26 +2,25 @@
|
||||
"data": [
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 7200,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 7200,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -38,6 +37,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -56,6 +56,7 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
@@ -63,36 +64,46 @@
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"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,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -114,7 +125,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -124,6 +137,7 @@
|
||||
"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,
|
||||
@@ -133,6 +147,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -144,14 +159,19 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 7200,
|
||||
"subtotal_excluding_tax": 7200,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 7200,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 7200,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
}
|
||||
],
|
||||
|
@@ -2,26 +2,25 @@
|
||||
"data": [
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 36000,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 36000,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -38,6 +37,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -56,43 +56,54 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"id": "attach_discount_to_realm--Event.list.7.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.6.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 6000,
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -114,7 +125,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -124,6 +137,7 @@
|
||||
"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,
|
||||
@@ -133,6 +147,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -144,38 +159,42 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 36000,
|
||||
"subtotal_excluding_tax": 36000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 36000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 36000,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
},
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 7200,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 7200,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -192,6 +211,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -210,6 +230,7 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
@@ -217,36 +238,46 @@
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"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,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -268,7 +299,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -278,6 +311,7 @@
|
||||
"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,
|
||||
@@ -287,6 +321,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -298,14 +333,19 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 7200,
|
||||
"subtotal_excluding_tax": 7200,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 7200,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 7200,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
}
|
||||
],
|
||||
|
@@ -2,26 +2,25 @@
|
||||
"data": [
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 24000,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 0,
|
||||
"amount_remaining": 24000,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 0,
|
||||
"attempted": false,
|
||||
"auto_advance": true,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -38,6 +37,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -56,43 +56,54 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 24000,
|
||||
"amount_excluding_tax": 24000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard - renewal",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Invoice.finalize_invoice.3.json",
|
||||
"id": "attach_discount_to_realm--Invoice.create.3.json",
|
||||
"invoice": "attach_discount_to_realm--Invoice.create.3.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1388631845,
|
||||
"start": 1357095845
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Invoice.create.3.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "4000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -106,7 +117,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": false,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -116,6 +129,7 @@
|
||||
"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,
|
||||
@@ -125,6 +139,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -136,38 +151,42 @@
|
||||
"paid_at": null,
|
||||
"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_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
},
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 36000,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 36000,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -184,6 +203,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -202,43 +222,54 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"id": "attach_discount_to_realm--Event.list.7.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.6.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 6000,
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -260,7 +291,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -270,6 +303,7 @@
|
||||
"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,
|
||||
@@ -279,6 +313,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -290,38 +325,42 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 36000,
|
||||
"subtotal_excluding_tax": 36000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 36000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 36000,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
},
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 7200,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 7200,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -338,6 +377,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -356,6 +396,7 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
@@ -363,36 +404,46 @@
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"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,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -414,7 +465,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -424,6 +477,7 @@
|
||||
"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,
|
||||
@@ -433,6 +487,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -444,14 +499,19 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 7200,
|
||||
"subtotal_excluding_tax": 7200,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 7200,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 7200,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
}
|
||||
],
|
||||
|
@@ -1,25 +1,24 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 7200,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 7200,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -36,6 +35,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -54,6 +54,7 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 7200,
|
||||
"amount_excluding_tax": 7200,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
@@ -61,36 +62,46 @@
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"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,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "1200"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -112,7 +123,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -122,6 +135,7 @@
|
||||
"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,
|
||||
@@ -131,6 +145,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -142,13 +157,18 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 7200,
|
||||
"subtotal_excluding_tax": 7200,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 7200,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 7200,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"webhooks_delivered_at": 1000000000
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": null
|
||||
}
|
||||
|
@@ -1,25 +1,24 @@
|
||||
{
|
||||
"account_country": "US",
|
||||
"account_name": "NORMALIZED",
|
||||
"account_name": "Kandra Labs, Inc.",
|
||||
"account_tax_ids": null,
|
||||
"amount_due": 36000,
|
||||
"amount_overpaid": 0,
|
||||
"amount_paid": 36000,
|
||||
"amount_remaining": 0,
|
||||
"amount_shipping": 0,
|
||||
"application": null,
|
||||
"application_fee_amount": null,
|
||||
"attempt_count": 1,
|
||||
"attempted": true,
|
||||
"auto_advance": false,
|
||||
"automatic_tax": {
|
||||
"disabled_reason": null,
|
||||
"enabled": false,
|
||||
"liability": null,
|
||||
"provider": null,
|
||||
"status": null
|
||||
},
|
||||
"automatically_finalizes_at": null,
|
||||
"billing_reason": "manual",
|
||||
"charge": "ch_NORMALIZED",
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
@@ -36,6 +35,7 @@
|
||||
"default_source": null,
|
||||
"default_tax_rates": [],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [],
|
||||
"due_date": null,
|
||||
"effective_at": 1000000000,
|
||||
@@ -54,43 +54,54 @@
|
||||
"data": [
|
||||
{
|
||||
"amount": 36000,
|
||||
"amount_excluding_tax": 36000,
|
||||
"currency": "usd",
|
||||
"description": "Zulip Cloud Standard",
|
||||
"discount_amounts": [],
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"id": "attach_discount_to_realm--Event.list.7.json",
|
||||
"invoice": "attach_discount_to_realm--Event.list.6.json",
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "line_item",
|
||||
"parent": {
|
||||
"invoice_item_details": {
|
||||
"invoice_item": "ii_NORMALIZED",
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"subscription": null
|
||||
},
|
||||
"subscription_item_details": null,
|
||||
"type": "invoice_item_details"
|
||||
},
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pretax_credit_amounts": [],
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 6000,
|
||||
"unit_amount_decimal": "6000"
|
||||
},
|
||||
"proration": false,
|
||||
"proration_details": {
|
||||
"credited_items": null
|
||||
},
|
||||
"quantity": 6,
|
||||
"taxes": []
|
||||
"subscription": null,
|
||||
"tax_amounts": [],
|
||||
"tax_rates": [],
|
||||
"type": "invoiceitem",
|
||||
"unit_amount_excluding_tax": "6000"
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
@@ -112,7 +123,9 @@
|
||||
"number": "NORMALIZED",
|
||||
"object": "invoice",
|
||||
"on_behalf_of": null,
|
||||
"parent": null,
|
||||
"paid": true,
|
||||
"paid_out_of_band": false,
|
||||
"payment_intent": "pi_NORMALIZED",
|
||||
"payment_settings": {
|
||||
"default_mandate": null,
|
||||
"payment_method_options": null,
|
||||
@@ -122,6 +135,7 @@
|
||||
"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,
|
||||
@@ -131,6 +145,7 @@
|
||||
"template": null,
|
||||
"template_version": null
|
||||
},
|
||||
"rendering_options": null,
|
||||
"shipping_cost": null,
|
||||
"shipping_details": null,
|
||||
"starting_balance": 0,
|
||||
@@ -142,13 +157,18 @@
|
||||
"paid_at": 1000000000,
|
||||
"voided_at": null
|
||||
},
|
||||
"subscription": null,
|
||||
"subscription_details": {
|
||||
"metadata": null
|
||||
},
|
||||
"subtotal": 36000,
|
||||
"subtotal_excluding_tax": 36000,
|
||||
"tax": null,
|
||||
"test_clock": null,
|
||||
"total": 36000,
|
||||
"total_discount_amounts": [],
|
||||
"total_excluding_tax": 36000,
|
||||
"total_pretax_credit_amounts": [],
|
||||
"total_taxes": [],
|
||||
"total_tax_amounts": [],
|
||||
"transfer_data": null,
|
||||
"webhooks_delivered_at": 1000000000
|
||||
}
|
||||
|
@@ -7,25 +7,41 @@
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"invoice": "in_NORMALIZED",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "invoiceitem",
|
||||
"parent": null,
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.2.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"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": [],
|
||||
"test_clock": null
|
||||
"test_clock": null,
|
||||
"unit_amount": 1200,
|
||||
"unit_amount_decimal": "1200"
|
||||
}
|
||||
|
@@ -7,25 +7,41 @@
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"invoice": "in_NORMALIZED",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "invoiceitem",
|
||||
"parent": null,
|
||||
"period": {
|
||||
"end": 1357095845,
|
||||
"start": 1325473445
|
||||
},
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Event.list.6.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"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": [],
|
||||
"test_clock": null
|
||||
"test_clock": null,
|
||||
"unit_amount": 6000,
|
||||
"unit_amount_decimal": "6000"
|
||||
}
|
||||
|
@@ -7,25 +7,41 @@
|
||||
"discountable": false,
|
||||
"discounts": [],
|
||||
"id": "attach_discount_to_realm--InvoiceItem.create.3.json",
|
||||
"invoice": "in_NORMALIZED",
|
||||
"invoice": null,
|
||||
"livemode": false,
|
||||
"metadata": {},
|
||||
"object": "invoiceitem",
|
||||
"parent": null,
|
||||
"period": {
|
||||
"end": 1388631845,
|
||||
"start": 1357095845
|
||||
},
|
||||
"pricing": {
|
||||
"price_details": {
|
||||
"price": "price_NORMALIZED",
|
||||
"product": "prod_NORMALIZED"
|
||||
},
|
||||
"type": "price_details",
|
||||
"plan": null,
|
||||
"price": {
|
||||
"active": false,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1000000000,
|
||||
"currency": "usd",
|
||||
"custom_unit_amount": null,
|
||||
"id": "attach_discount_to_realm--Invoice.create.3.json",
|
||||
"livemode": false,
|
||||
"lookup_key": null,
|
||||
"metadata": {},
|
||||
"nickname": null,
|
||||
"object": "price",
|
||||
"product": "prod_NORMALIZED",
|
||||
"recurring": null,
|
||||
"tax_behavior": "unspecified",
|
||||
"tiers_mode": null,
|
||||
"transform_quantity": null,
|
||||
"type": "one_time",
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
},
|
||||
"proration": false,
|
||||
"quantity": 6,
|
||||
"subscription": null,
|
||||
"tax_rates": [],
|
||||
"test_clock": null
|
||||
"test_clock": null,
|
||||
"unit_amount": 4000,
|
||||
"unit_amount_decimal": "4000"
|
||||
}
|
||||
|
@@ -11,8 +11,7 @@
|
||||
},
|
||||
"email": null,
|
||||
"name": "John Doe",
|
||||
"phone": null,
|
||||
"tax_id": null
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
@@ -35,7 +34,6 @@
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"regulated_status": "unregulated",
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user