threecommon 0.1.0__tar.gz → 0.3.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.
Files changed (40) hide show
  1. threecommon-0.3.0/CHANGELOG.md +62 -0
  2. {threecommon-0.1.0 → threecommon-0.3.0}/PKG-INFO +1 -1
  3. {threecommon-0.1.0 → threecommon-0.3.0}/pyproject.toml +1 -1
  4. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/client.py +7 -0
  5. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/invoices/__init__.py +8 -0
  6. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/invoices/service.py +89 -1
  7. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/invoices/types.py +42 -3
  8. threecommon-0.3.0/src/threecommon/subscriptions/__init__.py +57 -0
  9. threecommon-0.3.0/src/threecommon/subscriptions/service.py +376 -0
  10. threecommon-0.3.0/src/threecommon/subscriptions/types.py +359 -0
  11. threecommon-0.1.0/CHANGELOG.md +0 -16
  12. {threecommon-0.1.0 → threecommon-0.3.0}/.gitignore +0 -0
  13. {threecommon-0.1.0 → threecommon-0.3.0}/LICENSE +0 -0
  14. {threecommon-0.1.0 → threecommon-0.3.0}/README.md +0 -0
  15. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/__init__.py +0 -0
  16. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/_core/__init__.py +0 -0
  17. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/_core/headers.py +0 -0
  18. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/_core/http_client.py +0 -0
  19. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/_core/parse.py +0 -0
  20. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/_core/retry.py +0 -0
  21. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/_core/telemetry.py +0 -0
  22. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/_core/url.py +0 -0
  23. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/_generated/__init__.py +0 -0
  24. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/_generated/models.py +0 -0
  25. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/api_version.py +0 -0
  26. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/config.py +0 -0
  27. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/errors/__init__.py +0 -0
  28. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/errors/base.py +0 -0
  29. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/errors/classes.py +0 -0
  30. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/events/__init__.py +0 -0
  31. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/events/service.py +0 -0
  32. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/events/types.py +0 -0
  33. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/filters/__init__.py +0 -0
  34. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/filters/builder.py +0 -0
  35. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/filters/types.py +0 -0
  36. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/helpers.py +0 -0
  37. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/pagination/__init__.py +0 -0
  38. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/pagination/auto_paginator.py +0 -0
  39. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/py.typed +0 -0
  40. {threecommon-0.1.0 → threecommon-0.3.0}/src/threecommon/version.py +0 -0
@@ -0,0 +1,62 @@
1
+ # Changelog
2
+
3
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
4
+ versions follow [SemVer](https://semver.org/spec/v2.0.0.html).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## 0.3.0
9
+
10
+ ### Added
11
+
12
+ - Invoices: auto_charge, refund_payment, delete_draft methods (sync + async).
13
+ - Invoices: subscription_id filter on list().
14
+ - Invoices: AutoChargeOutcome, AutoChargeResult, DeletedInvoice, RefundBody types.
15
+
16
+ ## 0.2.0
17
+
18
+ ### Added
19
+
20
+ - Invoice write operations completing parity with the public REST surface, on
21
+ both the sync and async clients:
22
+ - `auto_charge` — off-session charge the customer's saved card
23
+ (`POST /v1/invoices/{id}/auto_charge`). A decline resolves with
24
+ `outcome="failed"` and a `failure_code` rather than raising; only network /
25
+ processor 5xx errors raise.
26
+ - `refund_payment` — refund all or part of a recorded payment
27
+ (`POST /v1/invoices/{id}/payments/{paymentId}/refunds`). Idempotent on
28
+ `body.idempotency_key`.
29
+ - `delete_draft` — permanently remove a draft invoice
30
+ (`DELETE /v1/invoices/{id}`).
31
+ - New public types on `threecommon.invoices`: `AutoChargeResult`,
32
+ `AutoChargeOutcome`, `RefundBody`, and `DeletedInvoice`.
33
+
34
+ ### Fixed
35
+
36
+ - `InvoiceStatus` now includes `payment_failed` (the state set after a failed
37
+ off-session auto-charge); it was previously missing.
38
+ - Invoice `ListParams` now accepts the `subscription_id` filter the API
39
+ supports; it was previously missing.
40
+
41
+ ## 0.1.0
42
+
43
+ ### Added
44
+
45
+ - Subscriptions resource. The new `client.subscriptions` surface covers the
46
+ full subscription lifecycle: `list`, `retrieve`, `create`, `update`
47
+ (mid-cycle change with proration), `activate`, `cancel`,
48
+ `cancel_immediately`, `mark_unpaid`, `bill`, `renew`,
49
+ `preview_upcoming_invoice`, and `list_auto_paginate`. Types and typed
50
+ errors match the events / invoices resources. Both sync and async surfaces.
51
+
52
+ ## 0.0.0
53
+
54
+ ### Added
55
+
56
+ - Initial scaffolding.
57
+ - `ThreeCommon` (sync) and `AsyncThreeCommon` (async) clients.
58
+ - Events resource: `list`, `retrieve`, `update`, `list_auto_paginate`.
59
+ - Invoices resource: `list`, `retrieve`, `create`, `update`, `finalize`, `void`,
60
+ `record_payment`, `list_auto_paginate`. Both sync and async surfaces.
61
+ - Typed exception tree (`AuthError`, `NotFoundError`, `RateLimitError`, …).
62
+ - Conformance harness running shared YAML scenarios against both clients.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threecommon
3
- Version: 0.1.0
3
+ Version: 0.3.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.1.0"
13
+ version = "0.3.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."""
@@ -8,7 +8,10 @@ service classes directly is supported for advanced wiring.
8
8
 
9
9
  from threecommon.invoices.service import AsyncInvoicesService, InvoicesService
10
10
  from threecommon.invoices.types import (
11
+ AutoChargeOutcome,
12
+ AutoChargeResult,
11
13
  CreateBody,
14
+ DeletedInvoice,
12
15
  Invoice,
13
16
  InvoiceCurrency,
14
17
  InvoiceLineItem,
@@ -17,6 +20,7 @@ from threecommon.invoices.types import (
17
20
  ListInvoicesResponse,
18
21
  ListParams,
19
22
  PaymentBody,
23
+ RefundBody,
20
24
  RetrieveParams,
21
25
  UpdateBody,
22
26
  VoidBody,
@@ -24,7 +28,10 @@ from threecommon.invoices.types import (
24
28
 
25
29
  __all__ = (
26
30
  "AsyncInvoicesService",
31
+ "AutoChargeOutcome",
32
+ "AutoChargeResult",
27
33
  "CreateBody",
34
+ "DeletedInvoice",
28
35
  "Invoice",
29
36
  "InvoiceCurrency",
30
37
  "InvoiceLineItem",
@@ -34,6 +41,7 @@ __all__ = (
34
41
  "ListInvoicesResponse",
35
42
  "ListParams",
36
43
  "PaymentBody",
44
+ "RefundBody",
37
45
  "RetrieveParams",
38
46
  "UpdateBody",
39
47
  "VoidBody",
@@ -6,17 +6,20 @@ difference is which HTTP client they call.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from typing import TYPE_CHECKING
9
+ from typing import TYPE_CHECKING, Any
10
10
  from urllib.parse import quote
11
11
 
12
12
  from threecommon._core.http_client import Request
13
13
  from threecommon.errors.classes import ValidationError
14
14
  from threecommon.invoices.types import (
15
+ AutoChargeResult,
15
16
  CreateBody,
17
+ DeletedInvoice,
16
18
  Invoice,
17
19
  ListInvoicesResponse,
18
20
  ListParams,
19
21
  PaymentBody,
22
+ RefundBody,
20
23
  RetrieveParams,
21
24
  UpdateBody,
22
25
  VoidBody,
@@ -56,6 +59,23 @@ def _action_path(invoice_id: str, action: str) -> str:
56
59
  return f"{_path_for(invoice_id)}/{action}"
57
60
 
58
61
 
62
+ def _refund_path(invoice_id: str, payment_id: str) -> str:
63
+ return f"{_path_for(invoice_id)}/payments/{quote(payment_id, safe='')}/refunds"
64
+
65
+
66
+ def _build_auto_charge_result(response: dict[str, Any]) -> AutoChargeResult:
67
+ payload: dict[str, Any] = {"invoice": response["data"], "outcome": response["outcome"]}
68
+ if response.get("failureCode") is not None:
69
+ payload["failure_code"] = response["failureCode"]
70
+ return AutoChargeResult.model_validate(payload)
71
+
72
+
73
+ def _require_payment_id(payment_id: str) -> None:
74
+ if not payment_id:
75
+ msg = "invoices.refund_payment: payment_id must be a non-empty string"
76
+ raise ValidationError(code="missing_payment_id", message=msg)
77
+
78
+
59
79
  # ────────────────────────────────────────────────────────────────────────────
60
80
  # Sync
61
81
  # ────────────────────────────────────────────────────────────────────────────
@@ -148,6 +168,48 @@ class InvoicesService:
148
168
  )
149
169
  return Invoice.model_validate(response["data"])
150
170
 
171
+ def auto_charge(self, invoice_id: str) -> AutoChargeResult:
172
+ """Off-session charge the customer's saved card for an open invoice.
173
+
174
+ A decline is not an error — it resolves with ``outcome="failed"`` and a
175
+ ``failure_code``, leaving the invoice in ``payment_failed``. Only
176
+ network / processor 5xx errors raise.
177
+ """
178
+ _require_id("auto_charge", invoice_id)
179
+ response = self._http.request(
180
+ Request(method="POST", path=_action_path(invoice_id, "auto_charge"), body={})
181
+ )
182
+ return _build_auto_charge_result(response)
183
+
184
+ def refund_payment(self, invoice_id: str, payment_id: str, body: RefundBody) -> Invoice:
185
+ """Refund all or part of a recorded payment on a paid invoice.
186
+
187
+ Idempotent on ``body.idempotency_key``: replays return the existing
188
+ refund without contacting the processor again.
189
+ """
190
+ _require_id("refund_payment", invoice_id)
191
+ _require_payment_id(payment_id)
192
+ if body is None:
193
+ raise ValidationError(
194
+ code="missing_body",
195
+ message="invoices.refund_payment: body must be non-None",
196
+ )
197
+ payload = body.model_dump(by_alias=True, exclude_none=True)
198
+ response = self._http.request(
199
+ Request(method="POST", path=_refund_path(invoice_id, payment_id), body=payload)
200
+ )
201
+ return Invoice.model_validate(response["data"])
202
+
203
+ def delete_draft(self, invoice_id: str) -> DeletedInvoice:
204
+ """Permanently delete a draft invoice.
205
+
206
+ Only legal while in ``draft`` (no number issued); finalized invoices
207
+ must be voided instead so the audit trail stays intact.
208
+ """
209
+ _require_id("delete_draft", invoice_id)
210
+ response = self._http.request(Request(method="DELETE", path=_path_for(invoice_id)))
211
+ return DeletedInvoice.model_validate(response["data"])
212
+
151
213
  def list_auto_paginate(self, params: ListParams | None = None) -> Iter[Invoice]:
152
214
  """Iterate every invoice matching ``params``, paging automatically."""
153
215
  start_page = params.page if params is not None and params.page is not None else 0
@@ -246,6 +308,32 @@ class AsyncInvoicesService:
246
308
  )
247
309
  return Invoice.model_validate(response["data"])
248
310
 
311
+ async def auto_charge(self, invoice_id: str) -> AutoChargeResult:
312
+ _require_id("auto_charge", invoice_id)
313
+ response = await self._http.request(
314
+ Request(method="POST", path=_action_path(invoice_id, "auto_charge"), body={})
315
+ )
316
+ return _build_auto_charge_result(response)
317
+
318
+ async def refund_payment(self, invoice_id: str, payment_id: str, body: RefundBody) -> Invoice:
319
+ _require_id("refund_payment", invoice_id)
320
+ _require_payment_id(payment_id)
321
+ if body is None:
322
+ raise ValidationError(
323
+ code="missing_body",
324
+ message="invoices.refund_payment: body must be non-None",
325
+ )
326
+ payload = body.model_dump(by_alias=True, exclude_none=True)
327
+ response = await self._http.request(
328
+ Request(method="POST", path=_refund_path(invoice_id, payment_id), body=payload)
329
+ )
330
+ return Invoice.model_validate(response["data"])
331
+
332
+ async def delete_draft(self, invoice_id: str) -> DeletedInvoice:
333
+ _require_id("delete_draft", invoice_id)
334
+ response = await self._http.request(Request(method="DELETE", path=_path_for(invoice_id)))
335
+ return DeletedInvoice.model_validate(response["data"])
336
+
249
337
  def list_auto_paginate(self, params: ListParams | None = None) -> AsyncIter[Invoice]:
250
338
  """Async iterate every invoice matching ``params``."""
251
339
  start_page = params.page if params is not None and params.page is not None else 0
@@ -11,13 +11,17 @@ from typing import Literal
11
11
 
12
12
  from pydantic import BaseModel, ConfigDict, Field
13
13
 
14
- #: Lifecycle status of an invoice. Future server-side values will arrive as
15
- #: raw strings until the SDK is updated.
16
- InvoiceStatus = Literal["draft", "open", "paid", "void"]
14
+ #: Lifecycle status of an invoice. ``payment_failed`` is set when an off-session
15
+ #: auto-charge attempt is rejected (decline / SCA / no card); the invoice is
16
+ #: still owed and can be retried or paid manually.
17
+ InvoiceStatus = Literal["draft", "open", "payment_failed", "paid", "void"]
17
18
 
18
19
  #: Invoice currency code; all line amounts must match.
19
20
  InvoiceCurrency = Literal["USD", "CAD"]
20
21
 
22
+ #: Outcome of an auto-charge attempt.
23
+ AutoChargeOutcome = Literal["paid", "failed"]
24
+
21
25
 
22
26
  class _BaseModel(BaseModel):
23
27
  """Shared config: accept snake_case or camelCase, ignore unknown fields."""
@@ -130,6 +134,9 @@ class ListParams(_BaseModel):
130
134
  customer_id: str | None = Field(
131
135
  default=None, serialization_alias="customerId", validation_alias="customerId"
132
136
  )
137
+ subscription_id: str | None = Field(
138
+ default=None, serialization_alias="subscriptionId", validation_alias="subscriptionId"
139
+ )
133
140
  issued_after: str | None = Field(
134
141
  default=None, serialization_alias="issuedAfter", validation_alias="issuedAfter"
135
142
  )
@@ -193,3 +200,35 @@ class PaymentBody(_BaseModel):
193
200
  default=None, serialization_alias="idempotencyKey", validation_alias="idempotencyKey"
194
201
  )
195
202
  note: str | None = None
203
+
204
+
205
+ class RefundBody(_BaseModel):
206
+ """Body accepted by ``POST /v1/invoices/{id}/payments/{paymentId}/refunds``."""
207
+
208
+ amount: int
209
+ reason: Literal["duplicate", "fraudulent", "requested_by_customer"] | None = None
210
+ note: str | None = None
211
+ idempotency_key: str | None = Field(
212
+ default=None, serialization_alias="idempotencyKey", validation_alias="idempotencyKey"
213
+ )
214
+
215
+
216
+ class AutoChargeResult(_BaseModel):
217
+ """Successful response shape from ``POST /v1/invoices/{id}/auto_charge``.
218
+
219
+ A card decline is an expected business outcome, not an error: ``outcome`` is
220
+ ``"failed"`` with the invoice left in ``payment_failed`` and a
221
+ ``failure_code`` set. Only network / processor 5xx errors raise.
222
+ """
223
+
224
+ invoice: Invoice
225
+ outcome: AutoChargeOutcome
226
+ failure_code: str | None = Field(
227
+ default=None, serialization_alias="failureCode", validation_alias="failureCode"
228
+ )
229
+
230
+
231
+ class DeletedInvoice(_BaseModel):
232
+ """Result of ``DELETE /v1/invoices/{id}`` — the id of the removed draft."""
233
+
234
+ id: str
@@ -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
@@ -1,16 +0,0 @@
1
- # Changelog
2
-
3
- Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
4
- versions follow [SemVer](https://semver.org/spec/v2.0.0.html).
5
-
6
- ## [Unreleased]
7
-
8
- ### Added
9
-
10
- - Initial scaffolding.
11
- - `ThreeCommon` (sync) and `AsyncThreeCommon` (async) clients.
12
- - Events resource: `list`, `retrieve`, `update`, `list_auto_paginate`.
13
- - Invoices resource: `list`, `retrieve`, `create`, `update`, `finalize`, `void`,
14
- `record_payment`, `list_auto_paginate`. Both sync and async surfaces.
15
- - Typed exception tree (`AuthError`, `NotFoundError`, `RateLimitError`, …).
16
- - Conformance harness running shared YAML scenarios against both clients.
File without changes
File without changes
File without changes