threecommon 0.2.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.2.0 → threecommon-0.3.0}/PKG-INFO +1 -1
  3. {threecommon-0.2.0 → threecommon-0.3.0}/pyproject.toml +1 -1
  4. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/invoices/__init__.py +8 -0
  5. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/invoices/service.py +89 -1
  6. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/invoices/types.py +42 -3
  7. threecommon-0.2.0/CHANGELOG.md +0 -29
  8. {threecommon-0.2.0 → threecommon-0.3.0}/.gitignore +0 -0
  9. {threecommon-0.2.0 → threecommon-0.3.0}/LICENSE +0 -0
  10. {threecommon-0.2.0 → threecommon-0.3.0}/README.md +0 -0
  11. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/__init__.py +0 -0
  12. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/_core/__init__.py +0 -0
  13. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/_core/headers.py +0 -0
  14. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/_core/http_client.py +0 -0
  15. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/_core/parse.py +0 -0
  16. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/_core/retry.py +0 -0
  17. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/_core/telemetry.py +0 -0
  18. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/_core/url.py +0 -0
  19. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/_generated/__init__.py +0 -0
  20. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/_generated/models.py +0 -0
  21. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/api_version.py +0 -0
  22. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/client.py +0 -0
  23. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/config.py +0 -0
  24. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/errors/__init__.py +0 -0
  25. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/errors/base.py +0 -0
  26. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/errors/classes.py +0 -0
  27. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/events/__init__.py +0 -0
  28. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/events/service.py +0 -0
  29. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/events/types.py +0 -0
  30. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/filters/__init__.py +0 -0
  31. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/filters/builder.py +0 -0
  32. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/filters/types.py +0 -0
  33. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/helpers.py +0 -0
  34. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/pagination/__init__.py +0 -0
  35. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/pagination/auto_paginator.py +0 -0
  36. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/py.typed +0 -0
  37. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/subscriptions/__init__.py +0 -0
  38. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/subscriptions/service.py +0 -0
  39. {threecommon-0.2.0 → threecommon-0.3.0}/src/threecommon/subscriptions/types.py +0 -0
  40. {threecommon-0.2.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.2.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.2.0"
13
+ version = "0.3.0"
14
14
  dependencies = [
15
15
  "httpx>=0.27,<1.0",
16
16
  "pydantic>=2.7,<3.0",
@@ -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
@@ -1,29 +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
- ## 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
-
21
- ### Added
22
-
23
- - Initial scaffolding.
24
- - `ThreeCommon` (sync) and `AsyncThreeCommon` (async) clients.
25
- - Events resource: `list`, `retrieve`, `update`, `list_auto_paginate`.
26
- - Invoices resource: `list`, `retrieve`, `create`, `update`, `finalize`, `void`,
27
- `record_payment`, `list_auto_paginate`. Both sync and async surfaces.
28
- - Typed exception tree (`AuthError`, `NotFoundError`, `RateLimitError`, …).
29
- - Conformance harness running shared YAML scenarios against both clients.
File without changes
File without changes
File without changes