From 60c822ec84a3c5b0c5a212b1e10e458c4a30295b Mon Sep 17 00:00:00 2001 From: Sander van Hooft <7265703+sandervanhooft@users.noreply.github.com> Date: Fri, 13 Nov 2026 16:56:40 +0000 Subject: [PATCH] Expose order-line related subscription and release 0.1.0a3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-sync openapi.yaml with the production bundle (SHA-256 5e648169…) and regenerate models. OrderLine gains related_resources: None on historical, unrelated or omitted lines, {subscription_id: None} for an unresolved signup. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d5d1-e5c4-72aa-a52b-f00addc3cdd1 Co-authored-by: Amp --- CHANGELOG.md | 7 ++++- README.md | 2 +- docs/Development.md | 7 +++-- openapi.yaml | 70 ++++++++++++++++++++++++++++++++++++++----- pyproject.toml | 2 +- src/vatly/_version.py | 2 +- src/vatly/models.py | 51 +++++++++++++++++++++++++++---- tests/test_sdk.py | 30 +++++++++++++++++++ uv.lock | 2 +- 9 files changed, 152 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45a73fd..9846b21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ # Changelog -## Unreleased +## 0.1.0a3 — 2026-11-13 +- Sync `openapi.yaml` with the production specification. +- Add `OrderLine.related_resources.subscription_id`: the subscription associated with a + signup, renewal or immediate-update line. `related_resources` is `None` on historical + and unrelated lines and when omitted; `subscription_id` is `None` on a signup line + whose subscription is not yet available. `product_type`/`product_id` are unchanged. - Clarify European positioning, installation and release documentation. ## 0.1.0a2 — 2026-11-12 diff --git a/README.md b/README.md index f54f10b..81cbe0b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ OpenAPI operations are covered. ## Installation ```bash -pip install vatly-api-python==0.1.0a2 +pip install vatly-api-python==0.1.0a3 ``` For local development: `uv sync --locked`. diff --git a/docs/Development.md b/docs/Development.md index b08857b..d98e44a 100644 --- a/docs/Development.md +++ b/docs/Development.md @@ -68,13 +68,14 @@ logging response bodies that may contain personal data. | Source | Revision | | --- | --- | -| Production OpenAPI, retrieved 2026-11-09 | [vatly-docs specification](https://github.com/vatly/vatly-docs/blob/7e71ade32d311b10f79e10c15d0ac03ae32bde97/public/openapi.yaml) | +| Production OpenAPI, retrieved 2026-11-13 | [vatly-docs specification](https://github.com/vatly/vatly-docs/blob/66c8205ea886e4cb48e8e3b2e988de3839c6d4d8/public/openapi.yaml) | | Original JS design reference | [SDK revision](https://github.com/vatly/vatly-api-js/commit/8b0b920c29f7e7b30bf71b9e379ec0a8a1a22028) | -The checked-in specification is byte-identical to that source. Its SHA-256 is: +The checked-in specification is byte-identical to that source and to the bundle +published by Vatly's production release. Its SHA-256 is: ```text -69640ac82bbba8d101e6aa882facd5f13aa5cf67f38508e5cea18abef03c4940 +5e648169e101edc8b3e088dc0e28d0f0a293e8a0ed057014c7336057d986def9 ``` The specification's generated-at date is not its retrieval date. diff --git a/openapi.yaml b/openapi.yaml index 0bd6c88..7c32bda 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1965,6 +1965,10 @@ paths: description: | Retrieves a specific order by its ID. Includes full order details with line items, totals, and billing information. + For a paid checkout, follow `Checkout.orderId` and inspect `lines[].relatedResources`. + Signup, renewal and immediate-update lines expose the same subscriptionId. + Product references are unchanged. Neither + creation, a paid order nor an ID alone proves activation or entitlement. tags: - Orders parameters: @@ -2009,7 +2013,7 @@ paths: lines: - id: order_item_Jk4pQrSvWm8NjLhYbUcP resource: orderline - description: Pro Monthly Subscription + description: Pro Monthly Subscription — initial checkout quantity: 1 basePrice: value: '29.00' @@ -2028,8 +2032,10 @@ paths: amount: value: '6.09' currency: EUR - productType: subscription - productId: subscription_Hn5xWqVfKm8RjTgYbUcP + productType: subscription_plan + productId: subscription_plan_Hn5xWqVfKm8RjTgYbUcP + relatedResources: + subscriptionId: subscription_Hn5xWqVfKm8RjTgYbUcP customerDetails: fullName: John Doe companyName: Acme Corp @@ -6767,7 +6773,14 @@ components: and expires after 72 hours; an expired link lets the customer request a fresh one by email. OrderLine: type: object - description: A line item in an order + description: | + A line item in an order. `productType`/`productId` retain their existing meaning: + initial subscription lines reference the plan; renewal and immediate-update + lines reference the existing subscription. `relatedResources` adds the exact + per-line subscription relationship without repeating the product reference. + Discounts remain components of their owning line. + Follow a paid Checkout's `orderId` to its order lines; Checkout is unchanged. + No historical backfill is performed. required: - id - resource @@ -6822,14 +6835,55 @@ components: - billing_details_verification - auth_request_product - null - description: The product type this line links to; null when the line has no product link. - example: subscription + description: | + Linked resource type. Initial subscription orders use + `subscription_plan`; renewals and immediate updates use `subscription`. + Existing values and new-write behavior are unchanged. + example: subscription_plan productId: type: - string - 'null' - description: The linked product's id; for a `subscription` line this is the public `subscription_…` id. - example: subscription_Hn5xWqVfKm8RjTgYbUcP + description: | + Linked resource ID: a plan ID when productType is subscription_plan, + a subscription ID when productType is subscription. Existing values and + new-write behavior are unchanged. + example: subscription_plan_Hn5xWqVfKm8RjTgYbUcP + relatedResources: + description: | + Additional per-line relationships. Null on historical lines without recorded + correlation and on unrelated lines (including one-off, billing-verification + and authorization-minimum lines). Older payloads may omit this property. + Signup, renewal and immediate-update lines use the same subscriptionId: + the subscription associated with this line, not a creation or entitlement + signal. On ordinary sale lines productType distinguishes initial signup + (subscription_plan) from renewal/update (subscription). It does not + distinguish a renewal from an immediate update. Reversal lines copy the + original relationship and product reference, not a new creation claim. + oneOf: + - type: object + additionalProperties: false + required: + - subscriptionId + properties: + subscriptionId: + type: + - string + - 'null' + description: | + Subscription associated with this line. On initial signup lines, + null while the exact subscription projection is unavailable, + including pending or skipped creation; null does not distinguish + these cases and is not proof that creation failed or was skipped. + The deterministic per-line identity is retained durably, so GET + reveals the ID after projection recovery without recreating the + paid order. Frozen webhook deliveries retain their original snapshot; + GET and newly generated webhooks use the current availability. + Separate same-plan checkout lines have distinct subscription IDs; + quantity is the quantity of one subscription, not a subscription count. + Creation does not imply activation, entitlement or immediate readability. + Validate ownership and status before granting access. + - type: 'null' InvoiceUpdateLink: type: object description: A signed URL for the customer to update invoice details (billing address, VAT number, company name) diff --git a/pyproject.toml b/pyproject.toml index 1b098f7..c8e7a1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "vatly-api-python" -version = "0.1.0a2" +version = "0.1.0a3" description = "Python SDK for Vatly, the European Merchant of Record for AI, software and digital products" keywords = ["vatly", "merchant-of-record", "billing", "payments", "vat", "ai", "sdk"] readme = "README.md" diff --git a/src/vatly/_version.py b/src/vatly/_version.py index b0e5b02..1822f2d 100644 --- a/src/vatly/_version.py +++ b/src/vatly/_version.py @@ -1 +1 @@ -__version__ = "0.1.0a2" +__version__ = "0.1.0a3" diff --git a/src/vatly/models.py b/src/vatly/models.py index b7ce344..8e39eb2 100644 --- a/src/vatly/models.py +++ b/src/vatly/models.py @@ -1061,6 +1061,33 @@ class Links4(VatlyModel): ] +class RelatedResources(VatlyModel): + """ + Additional per-line relationships. Null on historical lines without recorded + correlation and on unrelated lines (including one-off, billing-verification + and authorization-minimum lines). Older payloads may omit this property. + Signup, renewal and immediate-update lines use the same subscriptionId: + the subscription associated with this line, not a creation or entitlement + signal. On ordinary sale lines productType distinguishes initial signup + (subscription_plan) from renewal/update (subscription). It does not + distinguish a renewal from an immediate update. Reversal lines copy the + original relationship and product reference, not a new creation claim. + + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + subscription_id: Annotated[ + str | None, + Field( + alias="subscriptionId", + description="Subscription associated with this line. On initial signup lines,\nnull while the exact subscription projection is unavailable,\nincluding pending or skipped creation; null does not distinguish\nthese cases and is not proof that creation failed or was skipped.\nThe deterministic per-line identity is retained durably, so GET\nreveals the ID after projection recovery without recreating the\npaid order. Frozen webhook deliveries retain their original snapshot;\nGET and newly generated webhooks use the current availability.\nSeparate same-plan checkout lines have distinct subscription IDs;\nquantity is the quantity of one subscription, not a subscription count.\nCreation does not imply activation, entitlement or immediate readability.\nValidate ownership and status before granting access.\n", + ), + ] = None + + class InvoiceUpdateLink(VatlyModel): """ A signed URL for the customer to update invoice details (billing address, VAT number, company name) @@ -2117,7 +2144,14 @@ class CreateSubscriptionBillingUpdateLinkResponse(VatlyModel): class OrderLine(VatlyModel): """ - A line item in an order + A line item in an order. `productType`/`productId` retain their existing meaning: + initial subscription lines reference the plan; renewal and immediate-update + lines reference the existing subscription. `relatedResources` adds the exact + per-line subscription relationship without repeating the product reference. + Discounts remain components of their owning line. + Follow a paid Checkout's `orderId` to its order lines; Checkout is unchanged. + No historical backfill is performed. + """ model_config = ConfigDict( @@ -2163,16 +2197,23 @@ class OrderLine(VatlyModel): | None, Field( alias="productType", - description="The product type this line links to; null when the line has no product link.", - examples=["subscription"], + description="Linked resource type. Initial subscription orders use\n`subscription_plan`; renewals and immediate updates use `subscription`.\nExisting values and new-write behavior are unchanged.\n", + examples=["subscription_plan"], ), ] = None product_id: Annotated[ str | None, Field( alias="productId", - description="The linked product's id; for a `subscription` line this is the public `subscription_…` id.", - examples=["subscription_Hn5xWqVfKm8RjTgYbUcP"], + description="Linked resource ID: a plan ID when productType is subscription_plan,\na subscription ID when productType is subscription. Existing values and\nnew-write behavior are unchanged.\n", + examples=["subscription_plan_Hn5xWqVfKm8RjTgYbUcP"], + ), + ] = None + related_resources: Annotated[ + RelatedResources | None, + Field( + alias="relatedResources", + description="Additional per-line relationships. Null on historical lines without recorded\ncorrelation and on unrelated lines (including one-off, billing-verification\nand authorization-minimum lines). Older payloads may omit this property.\nSignup, renewal and immediate-update lines use the same subscriptionId:\nthe subscription associated with this line, not a creation or entitlement\nsignal. On ordinary sale lines productType distinguishes initial signup\n(subscription_plan) from renewal/update (subscription). It does not\ndistinguish a renewal from an immediate update. Reversal lines copy the\noriginal relationship and product reference, not a new creation claim.\n", ), ] = None diff --git a/tests/test_sdk.py b/tests/test_sdk.py index 1a18b5c..8f7a8a3 100644 --- a/tests/test_sdk.py +++ b/tests/test_sdk.py @@ -224,6 +224,36 @@ def handler(r): ) +def test_order_line_related_subscription_is_typed_and_nullable(spec): + order = example(spec["components"]["schemas"]["Order"], spec) + line = order["lines"][0] + order["lines"] = [ + { + **line, + "productType": "subscription_plan", + "productId": "subscription_plan_a", + "relatedResources": {"subscriptionId": "subscription_a"}, + }, + {**line, "productType": "subscription_plan", "relatedResources": {"subscriptionId": None}}, + {**line, "productType": "one_off_product", "relatedResources": None}, + line, # Older payloads omit relatedResources. + ] + with VatlyClient( + KEY, transport=httpx.MockTransport(lambda r: httpx.Response(200, json=order)) + ) as c: + signup, unresolved, unrelated, older = c.orders.get("order_a").lines + assert "relatedResources" not in line + assert signup.related_resources.subscription_id == "subscription_a" + assert signup.product_id == "subscription_plan_a" + assert unresolved.related_resources is not None + assert unresolved.related_resources.subscription_id is None + assert unrelated.related_resources is None + assert older.related_resources is None + wire = [ln.model_dump(by_alias=True, exclude_unset=True) for ln in (signup, unresolved)] + assert wire[0]["relatedResources"] == {"subscriptionId": "subscription_a"} + assert wire[1]["relatedResources"] == {"subscriptionId": None} + + @pytest.mark.parametrize( "status,error", [ diff --git a/uv.lock b/uv.lock index 91bb89d..81b1ba7 100644 --- a/uv.lock +++ b/uv.lock @@ -2017,7 +2017,7 @@ wheels = [ [[package]] name = "vatly-api-python" -version = "0.1.0a2" +version = "0.1.0a3" source = { editable = "." } dependencies = [ { name = "httpx" },