threecommon 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {threecommon-0.1.0 → threecommon-0.2.0}/CHANGELOG.md +13 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/PKG-INFO +1 -1
- {threecommon-0.1.0 → threecommon-0.2.0}/pyproject.toml +1 -1
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/client.py +7 -0
- threecommon-0.2.0/src/threecommon/subscriptions/__init__.py +57 -0
- threecommon-0.2.0/src/threecommon/subscriptions/service.py +376 -0
- threecommon-0.2.0/src/threecommon/subscriptions/types.py +359 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/.gitignore +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/LICENSE +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/README.md +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/__init__.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/_core/__init__.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/_core/headers.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/_core/http_client.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/_core/parse.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/_core/retry.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/_core/telemetry.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/_core/url.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/_generated/__init__.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/_generated/models.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/api_version.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/config.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/errors/__init__.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/errors/base.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/errors/classes.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/events/__init__.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/events/service.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/events/types.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/filters/__init__.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/filters/builder.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/filters/types.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/helpers.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/invoices/__init__.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/invoices/service.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/invoices/types.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/pagination/__init__.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/pagination/auto_paginator.py +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/py.typed +0 -0
- {threecommon-0.1.0 → threecommon-0.2.0}/src/threecommon/version.py +0 -0
|
@@ -5,6 +5,19 @@ versions follow [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## 0.1.0
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Subscriptions resource. The new `client.subscriptions` surface covers the
|
|
13
|
+
full subscription lifecycle: `list`, `retrieve`, `create`, `update`
|
|
14
|
+
(mid-cycle change with proration), `activate`, `cancel`,
|
|
15
|
+
`cancel_immediately`, `mark_unpaid`, `bill`, `renew`,
|
|
16
|
+
`preview_upcoming_invoice`, and `list_auto_paginate`. Types and typed
|
|
17
|
+
errors match the events / invoices resources. Both sync and async surfaces.
|
|
18
|
+
|
|
19
|
+
## 0.0.0
|
|
20
|
+
|
|
8
21
|
### Added
|
|
9
22
|
|
|
10
23
|
- Initial scaffolding.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: threecommon
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Official Python client for the 3Common Public API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/3-Common/sdk/tree/main/sdk-python
|
|
6
6
|
Project-URL: Issues, https://github.com/3-Common/sdk/issues
|
|
@@ -10,7 +10,7 @@ license = { text = "MIT" }
|
|
|
10
10
|
authors = [{ name = "3Common", email = "support@3common.com" }]
|
|
11
11
|
keywords = ["3common", "sdk", "api-client", "events", "invoices"]
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
|
-
version = "0.
|
|
13
|
+
version = "0.2.0"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"httpx>=0.27,<1.0",
|
|
16
16
|
"pydantic>=2.7,<3.0",
|
|
@@ -20,6 +20,7 @@ from threecommon._core.telemetry import Telemetry
|
|
|
20
20
|
from threecommon.config import RetryDelay, resolve_config
|
|
21
21
|
from threecommon.events.service import AsyncEventsService, EventsService
|
|
22
22
|
from threecommon.invoices.service import AsyncInvoicesService, InvoicesService
|
|
23
|
+
from threecommon.subscriptions.service import AsyncSubscriptionsService, SubscriptionsService
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING: # pragma: no cover
|
|
25
26
|
import logging
|
|
@@ -51,6 +52,9 @@ class ThreeCommon:
|
|
|
51
52
|
invoices: InvoicesService
|
|
52
53
|
"""Invoices resource — list, retrieve, create, update, finalize, void, record_payment."""
|
|
53
54
|
|
|
55
|
+
subscriptions: SubscriptionsService
|
|
56
|
+
"""Subscriptions resource — list, retrieve, create, update, activate, cancel, bill, renew."""
|
|
57
|
+
|
|
54
58
|
_http: HTTPClient
|
|
55
59
|
_telemetry: Telemetry
|
|
56
60
|
|
|
@@ -93,6 +97,7 @@ class ThreeCommon:
|
|
|
93
97
|
)
|
|
94
98
|
self.events = EventsService(self._http)
|
|
95
99
|
self.invoices = InvoicesService(self._http)
|
|
100
|
+
self.subscriptions = SubscriptionsService(self._http)
|
|
96
101
|
|
|
97
102
|
def close(self) -> None:
|
|
98
103
|
"""Close the underlying httpx client (no-op if you supplied your own)."""
|
|
@@ -120,6 +125,7 @@ class AsyncThreeCommon:
|
|
|
120
125
|
|
|
121
126
|
events: AsyncEventsService
|
|
122
127
|
invoices: AsyncInvoicesService
|
|
128
|
+
subscriptions: AsyncSubscriptionsService
|
|
123
129
|
|
|
124
130
|
_http: AsyncHTTPClient
|
|
125
131
|
_telemetry: Telemetry
|
|
@@ -163,6 +169,7 @@ class AsyncThreeCommon:
|
|
|
163
169
|
)
|
|
164
170
|
self.events = AsyncEventsService(self._http)
|
|
165
171
|
self.invoices = AsyncInvoicesService(self._http)
|
|
172
|
+
self.subscriptions = AsyncSubscriptionsService(self._http)
|
|
166
173
|
|
|
167
174
|
async def aclose(self) -> None:
|
|
168
175
|
"""Close the underlying async httpx client."""
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Subscriptions resource — sync and async clients plus public types.
|
|
2
|
+
|
|
3
|
+
Most callers reach this module through
|
|
4
|
+
[ThreeCommon.subscriptions][threecommon.ThreeCommon] /
|
|
5
|
+
[AsyncThreeCommon.subscriptions][threecommon.AsyncThreeCommon]; importing the
|
|
6
|
+
service classes directly is supported for advanced wiring.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from threecommon.subscriptions.service import (
|
|
10
|
+
AsyncSubscriptionsService,
|
|
11
|
+
SubscriptionsService,
|
|
12
|
+
)
|
|
13
|
+
from threecommon.subscriptions.types import (
|
|
14
|
+
BillSubscriptionResult,
|
|
15
|
+
CancelBody,
|
|
16
|
+
CancelImmediatelyBody,
|
|
17
|
+
CreateBody,
|
|
18
|
+
CreateBodyItem,
|
|
19
|
+
ListParams,
|
|
20
|
+
ListSubscriptionsResponse,
|
|
21
|
+
RenewSubscriptionResult,
|
|
22
|
+
RetrieveParams,
|
|
23
|
+
Subscription,
|
|
24
|
+
SubscriptionInvoicePreview,
|
|
25
|
+
SubscriptionInvoicePreviewLineItem,
|
|
26
|
+
SubscriptionInvoiceRef,
|
|
27
|
+
SubscriptionItem,
|
|
28
|
+
SubscriptionProration,
|
|
29
|
+
SubscriptionStatus,
|
|
30
|
+
SubscriptionTaxId,
|
|
31
|
+
UpdateBody,
|
|
32
|
+
UpdateSubscriptionResult,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = (
|
|
36
|
+
"AsyncSubscriptionsService",
|
|
37
|
+
"BillSubscriptionResult",
|
|
38
|
+
"CancelBody",
|
|
39
|
+
"CancelImmediatelyBody",
|
|
40
|
+
"CreateBody",
|
|
41
|
+
"CreateBodyItem",
|
|
42
|
+
"ListParams",
|
|
43
|
+
"ListSubscriptionsResponse",
|
|
44
|
+
"RenewSubscriptionResult",
|
|
45
|
+
"RetrieveParams",
|
|
46
|
+
"Subscription",
|
|
47
|
+
"SubscriptionInvoicePreview",
|
|
48
|
+
"SubscriptionInvoicePreviewLineItem",
|
|
49
|
+
"SubscriptionInvoiceRef",
|
|
50
|
+
"SubscriptionItem",
|
|
51
|
+
"SubscriptionProration",
|
|
52
|
+
"SubscriptionStatus",
|
|
53
|
+
"SubscriptionTaxId",
|
|
54
|
+
"SubscriptionsService",
|
|
55
|
+
"UpdateBody",
|
|
56
|
+
"UpdateSubscriptionResult",
|
|
57
|
+
)
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Sync and async subscriptions services.
|
|
2
|
+
|
|
3
|
+
Both services share the same wire shape and validation logic; the only
|
|
4
|
+
difference is which HTTP client they call.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
from urllib.parse import quote
|
|
11
|
+
|
|
12
|
+
from threecommon._core.http_client import Request
|
|
13
|
+
from threecommon.errors.classes import ValidationError
|
|
14
|
+
from threecommon.pagination import AsyncIter, Iter
|
|
15
|
+
from threecommon.subscriptions.types import (
|
|
16
|
+
BillSubscriptionResult,
|
|
17
|
+
CancelBody,
|
|
18
|
+
CancelImmediatelyBody,
|
|
19
|
+
CreateBody,
|
|
20
|
+
ListParams,
|
|
21
|
+
ListSubscriptionsResponse,
|
|
22
|
+
RenewSubscriptionResult,
|
|
23
|
+
RetrieveParams,
|
|
24
|
+
Subscription,
|
|
25
|
+
SubscriptionInvoicePreview,
|
|
26
|
+
UpdateBody,
|
|
27
|
+
UpdateSubscriptionResult,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from threecommon._core.http_client import AsyncHTTPClient, HTTPClient
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _encode_list_params(params: ListParams | None) -> dict[str, str] | None:
|
|
35
|
+
if params is None:
|
|
36
|
+
return None
|
|
37
|
+
raw = params.model_dump(by_alias=True, exclude_none=True)
|
|
38
|
+
if not raw:
|
|
39
|
+
return None
|
|
40
|
+
return {k: str(v) for k, v in raw.items()}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _encode_retrieve_params(params: RetrieveParams | None) -> dict[str, str] | None:
|
|
44
|
+
if params is None or params.fields is None:
|
|
45
|
+
return None
|
|
46
|
+
return {"fields": params.fields}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _require_id(method: str, subscription_id: str) -> None:
|
|
50
|
+
if not subscription_id:
|
|
51
|
+
msg = f"subscriptions.{method}: id must be a non-empty string"
|
|
52
|
+
raise ValidationError(code="missing_id", message=msg)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _path_for(subscription_id: str) -> str:
|
|
56
|
+
return f"/subscriptions/{quote(subscription_id, safe='')}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _action_path(subscription_id: str, action: str) -> str:
|
|
60
|
+
return f"{_path_for(subscription_id)}/{action}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _build_update_result(response: dict[str, Any]) -> UpdateSubscriptionResult:
|
|
64
|
+
payload: dict[str, Any] = {
|
|
65
|
+
"subscription": response["data"],
|
|
66
|
+
"proration": response["proration"],
|
|
67
|
+
}
|
|
68
|
+
if response.get("invoice") is not None:
|
|
69
|
+
payload["invoice"] = response["invoice"]
|
|
70
|
+
return UpdateSubscriptionResult.model_validate(payload)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _build_bill_result(response: dict[str, Any]) -> BillSubscriptionResult:
|
|
74
|
+
return BillSubscriptionResult.model_validate(
|
|
75
|
+
{"subscription": response["data"], "invoice": response["invoice"]}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _build_renew_result(response: dict[str, Any]) -> RenewSubscriptionResult:
|
|
80
|
+
payload: dict[str, Any] = {"subscription": response["data"]}
|
|
81
|
+
if response.get("invoice") is not None:
|
|
82
|
+
payload["invoice"] = response["invoice"]
|
|
83
|
+
return RenewSubscriptionResult.model_validate(payload)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
# Sync
|
|
88
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class SubscriptionsService:
|
|
92
|
+
"""Sync subscriptions service — bound as ``client.subscriptions`` on [ThreeCommon]."""
|
|
93
|
+
|
|
94
|
+
__slots__ = ("_http",)
|
|
95
|
+
|
|
96
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
97
|
+
self._http = http
|
|
98
|
+
|
|
99
|
+
def list(self, params: ListParams | None = None) -> ListSubscriptionsResponse:
|
|
100
|
+
"""List the host's subscriptions (one page).
|
|
101
|
+
|
|
102
|
+
For full iteration use
|
|
103
|
+
[list_auto_paginate][SubscriptionsService.list_auto_paginate].
|
|
104
|
+
"""
|
|
105
|
+
body = self._http.request(
|
|
106
|
+
Request(method="GET", path="/subscriptions", query=_encode_list_params(params))
|
|
107
|
+
)
|
|
108
|
+
return ListSubscriptionsResponse.model_validate(body)
|
|
109
|
+
|
|
110
|
+
def retrieve(self, subscription_id: str, params: RetrieveParams | None = None) -> Subscription:
|
|
111
|
+
"""Retrieve a single subscription by id."""
|
|
112
|
+
_require_id("retrieve", subscription_id)
|
|
113
|
+
body = self._http.request(
|
|
114
|
+
Request(
|
|
115
|
+
method="GET",
|
|
116
|
+
path=_path_for(subscription_id),
|
|
117
|
+
query=_encode_retrieve_params(params),
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
return Subscription.model_validate(body["data"])
|
|
121
|
+
|
|
122
|
+
def create(self, body: CreateBody) -> Subscription:
|
|
123
|
+
"""Create a new subscription against an active recurring Price."""
|
|
124
|
+
if body is None:
|
|
125
|
+
raise ValidationError(
|
|
126
|
+
code="missing_body", message="subscriptions.create: body must be non-None"
|
|
127
|
+
)
|
|
128
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
129
|
+
response = self._http.request(Request(method="POST", path="/subscriptions", body=payload))
|
|
130
|
+
return Subscription.model_validate(response["data"])
|
|
131
|
+
|
|
132
|
+
def update(self, subscription_id: str, body: UpdateBody) -> UpdateSubscriptionResult:
|
|
133
|
+
"""Apply a mid-cycle price/quantity change with Stripe-style daily proration."""
|
|
134
|
+
_require_id("update", subscription_id)
|
|
135
|
+
if body is None:
|
|
136
|
+
raise ValidationError(
|
|
137
|
+
code="missing_body", message="subscriptions.update: body must be non-None"
|
|
138
|
+
)
|
|
139
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
140
|
+
response = self._http.request(
|
|
141
|
+
Request(method="PATCH", path=_path_for(subscription_id), body=payload)
|
|
142
|
+
)
|
|
143
|
+
return _build_update_result(response)
|
|
144
|
+
|
|
145
|
+
def activate(self, subscription_id: str) -> Subscription:
|
|
146
|
+
"""Transition an incomplete or trialing subscription to ``active``."""
|
|
147
|
+
_require_id("activate", subscription_id)
|
|
148
|
+
response = self._http.request(
|
|
149
|
+
Request(method="POST", path=_action_path(subscription_id, "activate"), body={})
|
|
150
|
+
)
|
|
151
|
+
return Subscription.model_validate(response["data"])
|
|
152
|
+
|
|
153
|
+
def cancel(self, subscription_id: str, body: CancelBody | None = None) -> Subscription:
|
|
154
|
+
"""Schedule cancellation at the end of the current period. Idempotent."""
|
|
155
|
+
_require_id("cancel", subscription_id)
|
|
156
|
+
payload = body.model_dump(by_alias=True, exclude_none=True) if body is not None else {}
|
|
157
|
+
response = self._http.request(
|
|
158
|
+
Request(method="POST", path=_action_path(subscription_id, "cancel"), body=payload)
|
|
159
|
+
)
|
|
160
|
+
return Subscription.model_validate(response["data"])
|
|
161
|
+
|
|
162
|
+
def cancel_immediately(
|
|
163
|
+
self, subscription_id: str, body: CancelImmediatelyBody | None = None
|
|
164
|
+
) -> Subscription:
|
|
165
|
+
"""Admin override — terminate the subscription immediately."""
|
|
166
|
+
_require_id("cancel_immediately", subscription_id)
|
|
167
|
+
payload = body.model_dump(by_alias=True, exclude_none=True) if body is not None else {}
|
|
168
|
+
response = self._http.request(
|
|
169
|
+
Request(
|
|
170
|
+
method="POST",
|
|
171
|
+
path=_action_path(subscription_id, "cancel-immediately"),
|
|
172
|
+
body=payload,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
return Subscription.model_validate(response["data"])
|
|
176
|
+
|
|
177
|
+
def mark_unpaid(self, subscription_id: str) -> Subscription:
|
|
178
|
+
"""Admin override — mark a subscription ``unpaid`` (terminal)."""
|
|
179
|
+
_require_id("mark_unpaid", subscription_id)
|
|
180
|
+
response = self._http.request(
|
|
181
|
+
Request(method="POST", path=_action_path(subscription_id, "mark-unpaid"), body={})
|
|
182
|
+
)
|
|
183
|
+
return Subscription.model_validate(response["data"])
|
|
184
|
+
|
|
185
|
+
def bill(self, subscription_id: str) -> BillSubscriptionResult:
|
|
186
|
+
"""Generate a draft invoice for the current period without advancing it."""
|
|
187
|
+
_require_id("bill", subscription_id)
|
|
188
|
+
response = self._http.request(
|
|
189
|
+
Request(method="POST", path=_action_path(subscription_id, "bill"), body={})
|
|
190
|
+
)
|
|
191
|
+
return _build_bill_result(response)
|
|
192
|
+
|
|
193
|
+
def renew(self, subscription_id: str) -> RenewSubscriptionResult:
|
|
194
|
+
"""Advance the subscription to its next billing period and generate an invoice."""
|
|
195
|
+
_require_id("renew", subscription_id)
|
|
196
|
+
response = self._http.request(
|
|
197
|
+
Request(method="POST", path=_action_path(subscription_id, "renew"), body={})
|
|
198
|
+
)
|
|
199
|
+
return _build_renew_result(response)
|
|
200
|
+
|
|
201
|
+
def preview_upcoming_invoice(self, subscription_id: str) -> SubscriptionInvoicePreview | None:
|
|
202
|
+
"""Preview the invoice the next renewal will generate.
|
|
203
|
+
|
|
204
|
+
Returns ``None`` when the subscription is set to cancel at period end.
|
|
205
|
+
"""
|
|
206
|
+
_require_id("preview_upcoming_invoice", subscription_id)
|
|
207
|
+
response = self._http.request(
|
|
208
|
+
Request(method="GET", path=_action_path(subscription_id, "upcoming"))
|
|
209
|
+
)
|
|
210
|
+
invoice = response["data"].get("invoice")
|
|
211
|
+
if invoice is None:
|
|
212
|
+
return None
|
|
213
|
+
return SubscriptionInvoicePreview.model_validate(invoice)
|
|
214
|
+
|
|
215
|
+
def list_auto_paginate(self, params: ListParams | None = None) -> Iter[Subscription]:
|
|
216
|
+
"""Iterate every subscription matching ``params``, paging automatically."""
|
|
217
|
+
start_page = params.page if params is not None and params.page is not None else 0
|
|
218
|
+
|
|
219
|
+
def fetch(page: int) -> tuple[list[Subscription], bool]:
|
|
220
|
+
page_params = (
|
|
221
|
+
params.model_copy(update={"page": page})
|
|
222
|
+
if params is not None
|
|
223
|
+
else ListParams(page=page)
|
|
224
|
+
)
|
|
225
|
+
body = self._http.request(
|
|
226
|
+
Request(
|
|
227
|
+
method="GET",
|
|
228
|
+
path="/subscriptions",
|
|
229
|
+
query=_encode_list_params(page_params),
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
response = ListSubscriptionsResponse.model_validate(body)
|
|
233
|
+
return response.data, response.has_more
|
|
234
|
+
|
|
235
|
+
return Iter(fetch_page=fetch, start_page=start_page)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
239
|
+
# Async
|
|
240
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class AsyncSubscriptionsService:
|
|
244
|
+
"""Async subscriptions service — bound as ``client.subscriptions`` on [AsyncThreeCommon]."""
|
|
245
|
+
|
|
246
|
+
__slots__ = ("_http",)
|
|
247
|
+
|
|
248
|
+
def __init__(self, http: AsyncHTTPClient) -> None:
|
|
249
|
+
self._http = http
|
|
250
|
+
|
|
251
|
+
async def list(self, params: ListParams | None = None) -> ListSubscriptionsResponse:
|
|
252
|
+
body = await self._http.request(
|
|
253
|
+
Request(method="GET", path="/subscriptions", query=_encode_list_params(params))
|
|
254
|
+
)
|
|
255
|
+
return ListSubscriptionsResponse.model_validate(body)
|
|
256
|
+
|
|
257
|
+
async def retrieve(
|
|
258
|
+
self, subscription_id: str, params: RetrieveParams | None = None
|
|
259
|
+
) -> Subscription:
|
|
260
|
+
_require_id("retrieve", subscription_id)
|
|
261
|
+
body = await self._http.request(
|
|
262
|
+
Request(
|
|
263
|
+
method="GET",
|
|
264
|
+
path=_path_for(subscription_id),
|
|
265
|
+
query=_encode_retrieve_params(params),
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
return Subscription.model_validate(body["data"])
|
|
269
|
+
|
|
270
|
+
async def create(self, body: CreateBody) -> Subscription:
|
|
271
|
+
if body is None:
|
|
272
|
+
raise ValidationError(
|
|
273
|
+
code="missing_body", message="subscriptions.create: body must be non-None"
|
|
274
|
+
)
|
|
275
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
276
|
+
response = await self._http.request(
|
|
277
|
+
Request(method="POST", path="/subscriptions", body=payload)
|
|
278
|
+
)
|
|
279
|
+
return Subscription.model_validate(response["data"])
|
|
280
|
+
|
|
281
|
+
async def update(self, subscription_id: str, body: UpdateBody) -> UpdateSubscriptionResult:
|
|
282
|
+
_require_id("update", subscription_id)
|
|
283
|
+
if body is None:
|
|
284
|
+
raise ValidationError(
|
|
285
|
+
code="missing_body", message="subscriptions.update: body must be non-None"
|
|
286
|
+
)
|
|
287
|
+
payload = body.model_dump(by_alias=True, exclude_none=True)
|
|
288
|
+
response = await self._http.request(
|
|
289
|
+
Request(method="PATCH", path=_path_for(subscription_id), body=payload)
|
|
290
|
+
)
|
|
291
|
+
return _build_update_result(response)
|
|
292
|
+
|
|
293
|
+
async def activate(self, subscription_id: str) -> Subscription:
|
|
294
|
+
_require_id("activate", subscription_id)
|
|
295
|
+
response = await self._http.request(
|
|
296
|
+
Request(method="POST", path=_action_path(subscription_id, "activate"), body={})
|
|
297
|
+
)
|
|
298
|
+
return Subscription.model_validate(response["data"])
|
|
299
|
+
|
|
300
|
+
async def cancel(self, subscription_id: str, body: CancelBody | None = None) -> Subscription:
|
|
301
|
+
_require_id("cancel", subscription_id)
|
|
302
|
+
payload = body.model_dump(by_alias=True, exclude_none=True) if body is not None else {}
|
|
303
|
+
response = await self._http.request(
|
|
304
|
+
Request(method="POST", path=_action_path(subscription_id, "cancel"), body=payload)
|
|
305
|
+
)
|
|
306
|
+
return Subscription.model_validate(response["data"])
|
|
307
|
+
|
|
308
|
+
async def cancel_immediately(
|
|
309
|
+
self, subscription_id: str, body: CancelImmediatelyBody | None = None
|
|
310
|
+
) -> Subscription:
|
|
311
|
+
_require_id("cancel_immediately", subscription_id)
|
|
312
|
+
payload = body.model_dump(by_alias=True, exclude_none=True) if body is not None else {}
|
|
313
|
+
response = await self._http.request(
|
|
314
|
+
Request(
|
|
315
|
+
method="POST",
|
|
316
|
+
path=_action_path(subscription_id, "cancel-immediately"),
|
|
317
|
+
body=payload,
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
return Subscription.model_validate(response["data"])
|
|
321
|
+
|
|
322
|
+
async def mark_unpaid(self, subscription_id: str) -> Subscription:
|
|
323
|
+
_require_id("mark_unpaid", subscription_id)
|
|
324
|
+
response = await self._http.request(
|
|
325
|
+
Request(method="POST", path=_action_path(subscription_id, "mark-unpaid"), body={})
|
|
326
|
+
)
|
|
327
|
+
return Subscription.model_validate(response["data"])
|
|
328
|
+
|
|
329
|
+
async def bill(self, subscription_id: str) -> BillSubscriptionResult:
|
|
330
|
+
_require_id("bill", subscription_id)
|
|
331
|
+
response = await self._http.request(
|
|
332
|
+
Request(method="POST", path=_action_path(subscription_id, "bill"), body={})
|
|
333
|
+
)
|
|
334
|
+
return _build_bill_result(response)
|
|
335
|
+
|
|
336
|
+
async def renew(self, subscription_id: str) -> RenewSubscriptionResult:
|
|
337
|
+
_require_id("renew", subscription_id)
|
|
338
|
+
response = await self._http.request(
|
|
339
|
+
Request(method="POST", path=_action_path(subscription_id, "renew"), body={})
|
|
340
|
+
)
|
|
341
|
+
return _build_renew_result(response)
|
|
342
|
+
|
|
343
|
+
async def preview_upcoming_invoice(
|
|
344
|
+
self, subscription_id: str
|
|
345
|
+
) -> SubscriptionInvoicePreview | None:
|
|
346
|
+
_require_id("preview_upcoming_invoice", subscription_id)
|
|
347
|
+
response = await self._http.request(
|
|
348
|
+
Request(method="GET", path=_action_path(subscription_id, "upcoming"))
|
|
349
|
+
)
|
|
350
|
+
invoice = response["data"].get("invoice")
|
|
351
|
+
if invoice is None:
|
|
352
|
+
return None
|
|
353
|
+
return SubscriptionInvoicePreview.model_validate(invoice)
|
|
354
|
+
|
|
355
|
+
def list_auto_paginate(self, params: ListParams | None = None) -> AsyncIter[Subscription]:
|
|
356
|
+
"""Async iterate every subscription matching ``params``."""
|
|
357
|
+
start_page = params.page if params is not None and params.page is not None else 0
|
|
358
|
+
http = self._http
|
|
359
|
+
|
|
360
|
+
async def fetch(page: int) -> tuple[list[Subscription], bool]:
|
|
361
|
+
page_params = (
|
|
362
|
+
params.model_copy(update={"page": page})
|
|
363
|
+
if params is not None
|
|
364
|
+
else ListParams(page=page)
|
|
365
|
+
)
|
|
366
|
+
body = await http.request(
|
|
367
|
+
Request(
|
|
368
|
+
method="GET",
|
|
369
|
+
path="/subscriptions",
|
|
370
|
+
query=_encode_list_params(page_params),
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
response = ListSubscriptionsResponse.model_validate(body)
|
|
374
|
+
return response.data, response.has_more
|
|
375
|
+
|
|
376
|
+
return AsyncIter(fetch_page=fetch, start_page=start_page)
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Public types for the subscriptions resource.
|
|
2
|
+
|
|
3
|
+
Hand-curated Pydantic models that mirror the wire shape (camelCase aliases
|
|
4
|
+
preserved). All response models use ``extra="ignore"`` so newer server-side
|
|
5
|
+
fields don't break older SDK versions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
#: Lifecycle status of a subscription. Future server-side values will arrive
|
|
15
|
+
#: as raw strings until the SDK is updated.
|
|
16
|
+
SubscriptionStatus = Literal[
|
|
17
|
+
"incomplete",
|
|
18
|
+
"trialing",
|
|
19
|
+
"active",
|
|
20
|
+
"past_due",
|
|
21
|
+
"canceled",
|
|
22
|
+
"unpaid",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _BaseModel(BaseModel):
|
|
27
|
+
"""Shared config: accept snake_case or camelCase, ignore unknown fields."""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(
|
|
30
|
+
populate_by_name=True,
|
|
31
|
+
extra="ignore",
|
|
32
|
+
str_strip_whitespace=False,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SubscriptionItem(_BaseModel):
|
|
37
|
+
"""One billed item on a subscription."""
|
|
38
|
+
|
|
39
|
+
id: str
|
|
40
|
+
price_id: str = Field(serialization_alias="priceId", validation_alias="priceId")
|
|
41
|
+
quantity: int
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SubscriptionTaxId(_BaseModel):
|
|
45
|
+
"""Host tax-ID snapshot carried onto each renewal invoice."""
|
|
46
|
+
|
|
47
|
+
type: str
|
|
48
|
+
value: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Subscription(_BaseModel):
|
|
52
|
+
"""One subscription as returned by the API.
|
|
53
|
+
|
|
54
|
+
Optional fields are populated only when the server returned them — list
|
|
55
|
+
responses with a ``fields`` filter omit unrequested values.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
id: str
|
|
59
|
+
host_id: str | None = Field(
|
|
60
|
+
default=None, serialization_alias="hostId", validation_alias="hostId"
|
|
61
|
+
)
|
|
62
|
+
contact_id: str | None = Field(
|
|
63
|
+
default=None, serialization_alias="contactId", validation_alias="contactId"
|
|
64
|
+
)
|
|
65
|
+
customer_email: str | None = Field(
|
|
66
|
+
default=None, serialization_alias="customerEmail", validation_alias="customerEmail"
|
|
67
|
+
)
|
|
68
|
+
price_id: str | None = Field(
|
|
69
|
+
default=None, serialization_alias="priceId", validation_alias="priceId"
|
|
70
|
+
)
|
|
71
|
+
quantity: int | None = None
|
|
72
|
+
items: list[SubscriptionItem] | None = None
|
|
73
|
+
status: SubscriptionStatus | None = None
|
|
74
|
+
current_period_start: str | None = Field(
|
|
75
|
+
default=None,
|
|
76
|
+
serialization_alias="currentPeriodStart",
|
|
77
|
+
validation_alias="currentPeriodStart",
|
|
78
|
+
)
|
|
79
|
+
current_period_end: str | None = Field(
|
|
80
|
+
default=None,
|
|
81
|
+
serialization_alias="currentPeriodEnd",
|
|
82
|
+
validation_alias="currentPeriodEnd",
|
|
83
|
+
)
|
|
84
|
+
trial_start: str | None = Field(
|
|
85
|
+
default=None, serialization_alias="trialStart", validation_alias="trialStart"
|
|
86
|
+
)
|
|
87
|
+
trial_end: str | None = Field(
|
|
88
|
+
default=None, serialization_alias="trialEnd", validation_alias="trialEnd"
|
|
89
|
+
)
|
|
90
|
+
billing_cycle_anchor: str | None = Field(
|
|
91
|
+
default=None,
|
|
92
|
+
serialization_alias="billingCycleAnchor",
|
|
93
|
+
validation_alias="billingCycleAnchor",
|
|
94
|
+
)
|
|
95
|
+
cancel_at: str | None = Field(
|
|
96
|
+
default=None, serialization_alias="cancelAt", validation_alias="cancelAt"
|
|
97
|
+
)
|
|
98
|
+
cancel_at_period_end: bool | None = Field(
|
|
99
|
+
default=None,
|
|
100
|
+
serialization_alias="cancelAtPeriodEnd",
|
|
101
|
+
validation_alias="cancelAtPeriodEnd",
|
|
102
|
+
)
|
|
103
|
+
canceled_at: str | None = Field(
|
|
104
|
+
default=None, serialization_alias="canceledAt", validation_alias="canceledAt"
|
|
105
|
+
)
|
|
106
|
+
cancel_reason: str | None = Field(
|
|
107
|
+
default=None, serialization_alias="cancelReason", validation_alias="cancelReason"
|
|
108
|
+
)
|
|
109
|
+
ended_at: str | None = Field(
|
|
110
|
+
default=None, serialization_alias="endedAt", validation_alias="endedAt"
|
|
111
|
+
)
|
|
112
|
+
started_at: str | None = Field(
|
|
113
|
+
default=None, serialization_alias="startedAt", validation_alias="startedAt"
|
|
114
|
+
)
|
|
115
|
+
dunning_enabled: bool | None = Field(
|
|
116
|
+
default=None,
|
|
117
|
+
serialization_alias="dunningEnabled",
|
|
118
|
+
validation_alias="dunningEnabled",
|
|
119
|
+
)
|
|
120
|
+
first_failure_at: str | None = Field(
|
|
121
|
+
default=None,
|
|
122
|
+
serialization_alias="firstFailureAt",
|
|
123
|
+
validation_alias="firstFailureAt",
|
|
124
|
+
)
|
|
125
|
+
next_retry_at: str | None = Field(
|
|
126
|
+
default=None,
|
|
127
|
+
serialization_alias="nextRetryAt",
|
|
128
|
+
validation_alias="nextRetryAt",
|
|
129
|
+
)
|
|
130
|
+
retry_count: int | None = Field(
|
|
131
|
+
default=None, serialization_alias="retryCount", validation_alias="retryCount"
|
|
132
|
+
)
|
|
133
|
+
notes: str | None = None
|
|
134
|
+
tax_ids: list[SubscriptionTaxId] | None = Field(
|
|
135
|
+
default=None, serialization_alias="taxIds", validation_alias="taxIds"
|
|
136
|
+
)
|
|
137
|
+
auto_charge: bool | None = Field(
|
|
138
|
+
default=None, serialization_alias="autoCharge", validation_alias="autoCharge"
|
|
139
|
+
)
|
|
140
|
+
payment_due_days: int | None = Field(
|
|
141
|
+
default=None,
|
|
142
|
+
serialization_alias="paymentDueDays",
|
|
143
|
+
validation_alias="paymentDueDays",
|
|
144
|
+
)
|
|
145
|
+
tax_rate: float | None = Field(
|
|
146
|
+
default=None, serialization_alias="taxRate", validation_alias="taxRate"
|
|
147
|
+
)
|
|
148
|
+
metadata: dict[str, str] | None = None
|
|
149
|
+
created_at: str | None = Field(
|
|
150
|
+
default=None, serialization_alias="createdAt", validation_alias="createdAt"
|
|
151
|
+
)
|
|
152
|
+
updated_at: str | None = Field(
|
|
153
|
+
default=None, serialization_alias="updatedAt", validation_alias="updatedAt"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SubscriptionInvoiceRef(_BaseModel):
|
|
158
|
+
"""Slim invoice reference returned alongside renew/bill/update responses."""
|
|
159
|
+
|
|
160
|
+
id: str
|
|
161
|
+
status: str
|
|
162
|
+
total: int
|
|
163
|
+
currency: str
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SubscriptionProration(_BaseModel):
|
|
167
|
+
"""Proration summary returned by ``PATCH /v1/subscriptions/{id}``."""
|
|
168
|
+
|
|
169
|
+
net_amount_minor: int = Field(
|
|
170
|
+
serialization_alias="netAmountMinor", validation_alias="netAmountMinor"
|
|
171
|
+
)
|
|
172
|
+
days_remaining: int = Field(
|
|
173
|
+
serialization_alias="daysRemaining", validation_alias="daysRemaining"
|
|
174
|
+
)
|
|
175
|
+
days_in_cycle: int = Field(serialization_alias="daysInCycle", validation_alias="daysInCycle")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class SubscriptionInvoicePreviewLineItem(_BaseModel):
|
|
179
|
+
"""One line item on a subscription invoice preview."""
|
|
180
|
+
|
|
181
|
+
description: str
|
|
182
|
+
quantity: int
|
|
183
|
+
unit_amount: int = Field(serialization_alias="unitAmount", validation_alias="unitAmount")
|
|
184
|
+
product_id: str | None = Field(
|
|
185
|
+
default=None, serialization_alias="productId", validation_alias="productId"
|
|
186
|
+
)
|
|
187
|
+
price_id: str | None = Field(
|
|
188
|
+
default=None, serialization_alias="priceId", validation_alias="priceId"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SubscriptionInvoicePreview(_BaseModel):
|
|
193
|
+
"""Non-persisted projection of the invoice the next renewal will generate."""
|
|
194
|
+
|
|
195
|
+
customer_id: str = Field(serialization_alias="customerId", validation_alias="customerId")
|
|
196
|
+
subscription_id: str = Field(
|
|
197
|
+
serialization_alias="subscriptionId", validation_alias="subscriptionId"
|
|
198
|
+
)
|
|
199
|
+
currency: str
|
|
200
|
+
line_items: list[SubscriptionInvoicePreviewLineItem] = Field(
|
|
201
|
+
serialization_alias="lineItems", validation_alias="lineItems"
|
|
202
|
+
)
|
|
203
|
+
subtotal: int
|
|
204
|
+
total: int
|
|
205
|
+
period_start: str = Field(serialization_alias="periodStart", validation_alias="periodStart")
|
|
206
|
+
period_end: str = Field(serialization_alias="periodEnd", validation_alias="periodEnd")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class ListSubscriptionsResponse(_BaseModel):
|
|
210
|
+
"""Successful response shape from ``GET /v1/subscriptions``."""
|
|
211
|
+
|
|
212
|
+
data: list[Subscription]
|
|
213
|
+
has_more: bool = Field(serialization_alias="hasMore", validation_alias="hasMore")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class UpdateSubscriptionResult(_BaseModel):
|
|
217
|
+
"""Successful response shape from ``PATCH /v1/subscriptions/{id}``."""
|
|
218
|
+
|
|
219
|
+
subscription: Subscription
|
|
220
|
+
invoice: SubscriptionInvoiceRef | None = None
|
|
221
|
+
proration: SubscriptionProration
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class BillSubscriptionResult(_BaseModel):
|
|
225
|
+
"""Successful response shape from ``POST /v1/subscriptions/{id}/bill``."""
|
|
226
|
+
|
|
227
|
+
subscription: Subscription
|
|
228
|
+
invoice: SubscriptionInvoiceRef
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class RenewSubscriptionResult(_BaseModel):
|
|
232
|
+
"""Successful response shape from ``POST /v1/subscriptions/{id}/renew``."""
|
|
233
|
+
|
|
234
|
+
subscription: Subscription
|
|
235
|
+
invoice: SubscriptionInvoiceRef | None = None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class ListParams(_BaseModel):
|
|
239
|
+
"""Query parameters accepted by ``GET /v1/subscriptions``."""
|
|
240
|
+
|
|
241
|
+
page: int | None = None
|
|
242
|
+
page_size: int | None = Field(
|
|
243
|
+
default=None, serialization_alias="pageSize", validation_alias="pageSize"
|
|
244
|
+
)
|
|
245
|
+
status: SubscriptionStatus | None = None
|
|
246
|
+
contact_id: str | None = Field(
|
|
247
|
+
default=None, serialization_alias="contactId", validation_alias="contactId"
|
|
248
|
+
)
|
|
249
|
+
price_id: str | None = Field(
|
|
250
|
+
default=None, serialization_alias="priceId", validation_alias="priceId"
|
|
251
|
+
)
|
|
252
|
+
fields: str | None = None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class RetrieveParams(_BaseModel):
|
|
256
|
+
"""Query parameters accepted by ``GET /v1/subscriptions/{id}``."""
|
|
257
|
+
|
|
258
|
+
fields: str | None = None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class CreateBodyItem(_BaseModel):
|
|
262
|
+
"""One item on a multi-item subscription create body."""
|
|
263
|
+
|
|
264
|
+
price_id: str = Field(serialization_alias="priceId", validation_alias="priceId")
|
|
265
|
+
quantity: int | None = None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class CreateBody(_BaseModel):
|
|
269
|
+
"""Body accepted by ``POST /v1/subscriptions``."""
|
|
270
|
+
|
|
271
|
+
price_id: str | None = Field(
|
|
272
|
+
default=None, serialization_alias="priceId", validation_alias="priceId"
|
|
273
|
+
)
|
|
274
|
+
quantity: int | None = None
|
|
275
|
+
items: list[CreateBodyItem] | None = None
|
|
276
|
+
contact_id: str | None = Field(
|
|
277
|
+
default=None, serialization_alias="contactId", validation_alias="contactId"
|
|
278
|
+
)
|
|
279
|
+
customer_email: str | None = Field(
|
|
280
|
+
default=None,
|
|
281
|
+
serialization_alias="customerEmail",
|
|
282
|
+
validation_alias="customerEmail",
|
|
283
|
+
)
|
|
284
|
+
trial_days: int | None = Field(
|
|
285
|
+
default=None, serialization_alias="trialDays", validation_alias="trialDays"
|
|
286
|
+
)
|
|
287
|
+
billing_cycle_anchor: str | None = Field(
|
|
288
|
+
default=None,
|
|
289
|
+
serialization_alias="billingCycleAnchor",
|
|
290
|
+
validation_alias="billingCycleAnchor",
|
|
291
|
+
)
|
|
292
|
+
cancel_at: str | None = Field(
|
|
293
|
+
default=None, serialization_alias="cancelAt", validation_alias="cancelAt"
|
|
294
|
+
)
|
|
295
|
+
dunning_enabled: bool | None = Field(
|
|
296
|
+
default=None,
|
|
297
|
+
serialization_alias="dunningEnabled",
|
|
298
|
+
validation_alias="dunningEnabled",
|
|
299
|
+
)
|
|
300
|
+
notes: str | None = None
|
|
301
|
+
tax_ids: list[SubscriptionTaxId] | None = Field(
|
|
302
|
+
default=None, serialization_alias="taxIds", validation_alias="taxIds"
|
|
303
|
+
)
|
|
304
|
+
auto_charge: bool | None = Field(
|
|
305
|
+
default=None, serialization_alias="autoCharge", validation_alias="autoCharge"
|
|
306
|
+
)
|
|
307
|
+
payment_due_days: int | None = Field(
|
|
308
|
+
default=None,
|
|
309
|
+
serialization_alias="paymentDueDays",
|
|
310
|
+
validation_alias="paymentDueDays",
|
|
311
|
+
)
|
|
312
|
+
tax_rate: float | None = Field(
|
|
313
|
+
default=None, serialization_alias="taxRate", validation_alias="taxRate"
|
|
314
|
+
)
|
|
315
|
+
metadata: dict[str, str] | None = None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class UpdateBody(_BaseModel):
|
|
319
|
+
"""Body accepted by ``PATCH /v1/subscriptions/{id}``.
|
|
320
|
+
|
|
321
|
+
Only fields you provide are changed.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
price_id: str | None = Field(
|
|
325
|
+
default=None, serialization_alias="priceId", validation_alias="priceId"
|
|
326
|
+
)
|
|
327
|
+
quantity: int | None = None
|
|
328
|
+
notes: str | None = None
|
|
329
|
+
tax_ids: list[SubscriptionTaxId] | None = Field(
|
|
330
|
+
default=None, serialization_alias="taxIds", validation_alias="taxIds"
|
|
331
|
+
)
|
|
332
|
+
tax_rate: float | None = Field(
|
|
333
|
+
default=None, serialization_alias="taxRate", validation_alias="taxRate"
|
|
334
|
+
)
|
|
335
|
+
auto_charge: bool | None = Field(
|
|
336
|
+
default=None, serialization_alias="autoCharge", validation_alias="autoCharge"
|
|
337
|
+
)
|
|
338
|
+
dunning_enabled: bool | None = Field(
|
|
339
|
+
default=None,
|
|
340
|
+
serialization_alias="dunningEnabled",
|
|
341
|
+
validation_alias="dunningEnabled",
|
|
342
|
+
)
|
|
343
|
+
payment_due_days: int | None = Field(
|
|
344
|
+
default=None,
|
|
345
|
+
serialization_alias="paymentDueDays",
|
|
346
|
+
validation_alias="paymentDueDays",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class CancelBody(_BaseModel):
|
|
351
|
+
"""Body accepted by ``POST /v1/subscriptions/{id}/cancel``."""
|
|
352
|
+
|
|
353
|
+
reason: str | None = None
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class CancelImmediatelyBody(_BaseModel):
|
|
357
|
+
"""Body accepted by ``POST /v1/subscriptions/{id}/cancel-immediately``."""
|
|
358
|
+
|
|
359
|
+
reason: str | None = None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|