diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index eba8a049..1887bd54 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "3.21.0"
+ ".": "3.22.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 8d440bd0..1265d475 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 114
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/orb%2Forb-faced218bf2dee89c8449bdb209e7090452d26c0646f0b998f84fe5bd3c4b7cb.yml
-openapi_spec_hash: b3a957e9c012fad5093545f65614ea42
-config_hash: e63f2d098e5d12f63ae4cd8270aa5c3c
+configured_endpoints: 115
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/orb%2Forb-d513be954ba2cb57489bd048f93790cabb13849f0f04d328a5a15694c99550df.yml
+openapi_spec_hash: 0d42694f412abf65defc3f88646a809c
+config_hash: 1e2186b09e57d7d27b6ab5c8e6410b31
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c332d1d5..c4d57af7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## 3.22.0 (2025-06-02)
+
+Full Changelog: [v3.21.0...v3.22.0](https://github.com/orbcorp/orb-python/compare/v3.21.0...v3.22.0)
+
+### Features
+
+* **api:** api update ([3deb3ab](https://github.com/orbcorp/orb-python/commit/3deb3ab6bf04d89551aa40deba99afcd29ee8099))
+* **client:** add follow_redirects request option ([5185843](https://github.com/orbcorp/orb-python/commit/51858434884f222b3c9974529aaf1958e5fba808))
+
+
+### Chores
+
+* **docs:** remove reference to rye shell ([c0cdb68](https://github.com/orbcorp/orb-python/commit/c0cdb684679416fb188bc77f8e822f8bcb345b50))
+
## 3.21.0 (2025-05-30)
Full Changelog: [v3.20.0...v3.21.0](https://github.com/orbcorp/orb-python/compare/v3.20.0...v3.21.0)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index abbbea65..ea9f566f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,8 +17,7 @@ $ rye sync --all-features
You can then run scripts using `rye run python script.py` or by activating the virtual environment:
```sh
-$ rye shell
-# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work
+# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work
$ source .venv/bin/activate
# now you can omit the `rye run` prefix
diff --git a/api.md b/api.md
index 4a380b71..b7905aed 100644
--- a/api.md
+++ b/api.md
@@ -365,6 +365,7 @@ from orb.types import (
SubscriptionFetchCostsResponse,
SubscriptionFetchScheduleResponse,
SubscriptionPriceIntervalsResponse,
+ SubscriptionRedeemCouponResponse,
SubscriptionSchedulePlanChangeResponse,
SubscriptionTriggerPhaseResponse,
SubscriptionUnscheduleCancellationResponse,
@@ -386,6 +387,7 @@ Methods:
- client.subscriptions.fetch_schedule(subscription_id, \*\*params) -> SyncPage[SubscriptionFetchScheduleResponse]
- client.subscriptions.fetch_usage(subscription_id, \*\*params) -> SubscriptionUsage
- client.subscriptions.price_intervals(subscription_id, \*\*params) -> SubscriptionPriceIntervalsResponse
+- client.subscriptions.redeem_coupon(subscription_id, \*\*params) -> SubscriptionRedeemCouponResponse
- client.subscriptions.schedule_plan_change(subscription_id, \*\*params) -> SubscriptionSchedulePlanChangeResponse
- client.subscriptions.trigger_phase(subscription_id, \*\*params) -> SubscriptionTriggerPhaseResponse
- client.subscriptions.unschedule_cancellation(subscription_id) -> SubscriptionUnscheduleCancellationResponse
diff --git a/pyproject.toml b/pyproject.toml
index 0cc737d9..9349e3c2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "orb-billing"
-version = "3.21.0"
+version = "3.22.0"
description = "The official Python library for the orb API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/orb/_base_client.py b/src/orb/_base_client.py
index f82fa056..056620f0 100644
--- a/src/orb/_base_client.py
+++ b/src/orb/_base_client.py
@@ -961,6 +961,9 @@ def request(
if self.custom_auth is not None:
kwargs["auth"] = self.custom_auth
+ if options.follow_redirects is not None:
+ kwargs["follow_redirects"] = options.follow_redirects
+
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
response = None
@@ -1475,6 +1478,9 @@ async def request(
if self.custom_auth is not None:
kwargs["auth"] = self.custom_auth
+ if options.follow_redirects is not None:
+ kwargs["follow_redirects"] = options.follow_redirects
+
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
response = None
diff --git a/src/orb/_models.py b/src/orb/_models.py
index 798956f1..4f214980 100644
--- a/src/orb/_models.py
+++ b/src/orb/_models.py
@@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
idempotency_key: str
json_data: Body
extra_json: AnyMapping
+ follow_redirects: bool
@final
@@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel):
files: Union[HttpxRequestFiles, None] = None
idempotency_key: Union[str, None] = None
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
+ follow_redirects: Union[bool, None] = None
# It should be noted that we cannot use `json` here as that would override
# a BaseModel method in an incompatible fashion.
diff --git a/src/orb/_types.py b/src/orb/_types.py
index a7f6c8ac..545d7960 100644
--- a/src/orb/_types.py
+++ b/src/orb/_types.py
@@ -101,6 +101,7 @@ class RequestOptions(TypedDict, total=False):
params: Query
extra_json: AnyMapping
idempotency_key: str
+ follow_redirects: bool
# Sentinel class used until PEP 0661 is accepted
@@ -217,3 +218,4 @@ class _GenericAlias(Protocol):
class HttpxSendArgs(TypedDict, total=False):
auth: httpx.Auth
+ follow_redirects: bool
diff --git a/src/orb/_version.py b/src/orb/_version.py
index 14eb9911..36b0ea85 100644
--- a/src/orb/_version.py
+++ b/src/orb/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "orb"
-__version__ = "3.21.0" # x-release-please-version
+__version__ = "3.22.0" # x-release-please-version
diff --git a/src/orb/resources/subscriptions.py b/src/orb/resources/subscriptions.py
index 8a6bde4e..7d1edbb5 100644
--- a/src/orb/resources/subscriptions.py
+++ b/src/orb/resources/subscriptions.py
@@ -17,6 +17,7 @@
subscription_fetch_costs_params,
subscription_fetch_usage_params,
subscription_update_trial_params,
+ subscription_redeem_coupon_params,
subscription_trigger_phase_params,
subscription_fetch_schedule_params,
subscription_price_intervals_params,
@@ -37,6 +38,7 @@
from ..types.subscription_create_response import SubscriptionCreateResponse
from ..types.subscription_fetch_costs_response import SubscriptionFetchCostsResponse
from ..types.subscription_update_trial_response import SubscriptionUpdateTrialResponse
+from ..types.subscription_redeem_coupon_response import SubscriptionRedeemCouponResponse
from ..types.subscription_trigger_phase_response import SubscriptionTriggerPhaseResponse
from ..types.subscription_fetch_schedule_response import SubscriptionFetchScheduleResponse
from ..types.subscription_price_intervals_response import SubscriptionPriceIntervalsResponse
@@ -1361,6 +1363,68 @@ def price_intervals(
cast_to=SubscriptionPriceIntervalsResponse,
)
+ def redeem_coupon(
+ self,
+ subscription_id: str,
+ *,
+ change_option: Literal["requested_date", "end_of_subscription_term", "immediate"],
+ coupon_id: str,
+ allow_invoice_credit_or_void: Optional[bool] | NotGiven = NOT_GIVEN,
+ change_date: Union[str, datetime, None] | NotGiven = NOT_GIVEN,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ idempotency_key: str | None = None,
+ ) -> SubscriptionRedeemCouponResponse:
+ """
+ Redeem a coupon effective at a given time.
+
+ Args:
+ coupon_id: Coupon ID to be redeemed for this subscription.
+
+ allow_invoice_credit_or_void: If false, this request will fail if it would void an issued invoice or create a
+ credit note. Consider using this as a safety mechanism if you do not expect
+ existing invoices to be changed.
+
+ change_date: The date that the coupon discount should take effect. This parameter can only be
+ passed if the `change_option` is `requested_date`.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ if not subscription_id:
+ raise ValueError(f"Expected a non-empty value for `subscription_id` but received {subscription_id!r}")
+ return self._post(
+ f"/subscriptions/{subscription_id}/redeem_coupon",
+ body=maybe_transform(
+ {
+ "change_option": change_option,
+ "coupon_id": coupon_id,
+ "allow_invoice_credit_or_void": allow_invoice_credit_or_void,
+ "change_date": change_date,
+ },
+ subscription_redeem_coupon_params.SubscriptionRedeemCouponParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=SubscriptionRedeemCouponResponse,
+ )
+
def schedule_plan_change(
self,
subscription_id: str,
@@ -3385,6 +3449,68 @@ async def price_intervals(
cast_to=SubscriptionPriceIntervalsResponse,
)
+ async def redeem_coupon(
+ self,
+ subscription_id: str,
+ *,
+ change_option: Literal["requested_date", "end_of_subscription_term", "immediate"],
+ coupon_id: str,
+ allow_invoice_credit_or_void: Optional[bool] | NotGiven = NOT_GIVEN,
+ change_date: Union[str, datetime, None] | NotGiven = NOT_GIVEN,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ idempotency_key: str | None = None,
+ ) -> SubscriptionRedeemCouponResponse:
+ """
+ Redeem a coupon effective at a given time.
+
+ Args:
+ coupon_id: Coupon ID to be redeemed for this subscription.
+
+ allow_invoice_credit_or_void: If false, this request will fail if it would void an issued invoice or create a
+ credit note. Consider using this as a safety mechanism if you do not expect
+ existing invoices to be changed.
+
+ change_date: The date that the coupon discount should take effect. This parameter can only be
+ passed if the `change_option` is `requested_date`.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ if not subscription_id:
+ raise ValueError(f"Expected a non-empty value for `subscription_id` but received {subscription_id!r}")
+ return await self._post(
+ f"/subscriptions/{subscription_id}/redeem_coupon",
+ body=await async_maybe_transform(
+ {
+ "change_option": change_option,
+ "coupon_id": coupon_id,
+ "allow_invoice_credit_or_void": allow_invoice_credit_or_void,
+ "change_date": change_date,
+ },
+ subscription_redeem_coupon_params.SubscriptionRedeemCouponParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=SubscriptionRedeemCouponResponse,
+ )
+
async def schedule_plan_change(
self,
subscription_id: str,
@@ -4132,6 +4258,9 @@ def __init__(self, subscriptions: Subscriptions) -> None:
self.price_intervals = _legacy_response.to_raw_response_wrapper(
subscriptions.price_intervals,
)
+ self.redeem_coupon = _legacy_response.to_raw_response_wrapper(
+ subscriptions.redeem_coupon,
+ )
self.schedule_plan_change = _legacy_response.to_raw_response_wrapper(
subscriptions.schedule_plan_change,
)
@@ -4186,6 +4315,9 @@ def __init__(self, subscriptions: AsyncSubscriptions) -> None:
self.price_intervals = _legacy_response.async_to_raw_response_wrapper(
subscriptions.price_intervals,
)
+ self.redeem_coupon = _legacy_response.async_to_raw_response_wrapper(
+ subscriptions.redeem_coupon,
+ )
self.schedule_plan_change = _legacy_response.async_to_raw_response_wrapper(
subscriptions.schedule_plan_change,
)
@@ -4240,6 +4372,9 @@ def __init__(self, subscriptions: Subscriptions) -> None:
self.price_intervals = to_streamed_response_wrapper(
subscriptions.price_intervals,
)
+ self.redeem_coupon = to_streamed_response_wrapper(
+ subscriptions.redeem_coupon,
+ )
self.schedule_plan_change = to_streamed_response_wrapper(
subscriptions.schedule_plan_change,
)
@@ -4294,6 +4429,9 @@ def __init__(self, subscriptions: AsyncSubscriptions) -> None:
self.price_intervals = async_to_streamed_response_wrapper(
subscriptions.price_intervals,
)
+ self.redeem_coupon = async_to_streamed_response_wrapper(
+ subscriptions.redeem_coupon,
+ )
self.schedule_plan_change = async_to_streamed_response_wrapper(
subscriptions.schedule_plan_change,
)
diff --git a/src/orb/types/__init__.py b/src/orb/types/__init__.py
index 76860045..3105fa6c 100644
--- a/src/orb/types/__init__.py
+++ b/src/orb/types/__init__.py
@@ -86,6 +86,7 @@
from .subscription_update_trial_params import SubscriptionUpdateTrialParams as SubscriptionUpdateTrialParams
from .invoice_line_item_create_response import InvoiceLineItemCreateResponse as InvoiceLineItemCreateResponse
from .subscription_fetch_costs_response import SubscriptionFetchCostsResponse as SubscriptionFetchCostsResponse
+from .subscription_redeem_coupon_params import SubscriptionRedeemCouponParams as SubscriptionRedeemCouponParams
from .subscription_trigger_phase_params import SubscriptionTriggerPhaseParams as SubscriptionTriggerPhaseParams
from .subscription_change_apply_response import SubscriptionChangeApplyResponse as SubscriptionChangeApplyResponse
from .subscription_fetch_schedule_params import SubscriptionFetchScheduleParams as SubscriptionFetchScheduleParams
@@ -93,6 +94,7 @@
from .dimensional_price_group_list_params import DimensionalPriceGroupListParams as DimensionalPriceGroupListParams
from .subscription_change_cancel_response import SubscriptionChangeCancelResponse as SubscriptionChangeCancelResponse
from .subscription_price_intervals_params import SubscriptionPriceIntervalsParams as SubscriptionPriceIntervalsParams
+from .subscription_redeem_coupon_response import SubscriptionRedeemCouponResponse as SubscriptionRedeemCouponResponse
from .subscription_trigger_phase_response import SubscriptionTriggerPhaseResponse as SubscriptionTriggerPhaseResponse
from .alert_create_for_subscription_params import AlertCreateForSubscriptionParams as AlertCreateForSubscriptionParams
from .beta_set_default_plan_version_params import BetaSetDefaultPlanVersionParams as BetaSetDefaultPlanVersionParams
diff --git a/src/orb/types/invoice.py b/src/orb/types/invoice.py
index 7d8e108b..05d91a75 100644
--- a/src/orb/types/invoice.py
+++ b/src/orb/types/invoice.py
@@ -760,7 +760,7 @@ class LineItem(BaseModel):
partially_invoiced_amount: str
"""Any amount applied from a partial invoice"""
- price: Optional[Price] = None
+ price: Price
"""
The Price resource represents a price that can be billed on a subscription,
resulting in a charge on an invoice in the form of an invoice line item. Prices
diff --git a/src/orb/types/invoice_fetch_upcoming_response.py b/src/orb/types/invoice_fetch_upcoming_response.py
index 5d9d2d6d..d2b4fa6a 100644
--- a/src/orb/types/invoice_fetch_upcoming_response.py
+++ b/src/orb/types/invoice_fetch_upcoming_response.py
@@ -760,7 +760,7 @@ class LineItem(BaseModel):
partially_invoiced_amount: str
"""Any amount applied from a partial invoice"""
- price: Optional[Price] = None
+ price: Price
"""
The Price resource represents a price that can be billed on a subscription,
resulting in a charge on an invoice in the form of an invoice line item. Prices
diff --git a/src/orb/types/invoice_line_item_create_response.py b/src/orb/types/invoice_line_item_create_response.py
index d8e6209d..3523a409 100644
--- a/src/orb/types/invoice_line_item_create_response.py
+++ b/src/orb/types/invoice_line_item_create_response.py
@@ -462,7 +462,7 @@ class InvoiceLineItemCreateResponse(BaseModel):
partially_invoiced_amount: str
"""Any amount applied from a partial invoice"""
- price: Optional[Price] = None
+ price: Price
"""
The Price resource represents a price that can be billed on a subscription,
resulting in a charge on an invoice in the form of an invoice line item. Prices
diff --git a/src/orb/types/subscription_redeem_coupon_params.py b/src/orb/types/subscription_redeem_coupon_params.py
new file mode 100644
index 00000000..385a6e26
--- /dev/null
+++ b/src/orb/types/subscription_redeem_coupon_params.py
@@ -0,0 +1,31 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from .._utils import PropertyInfo
+
+__all__ = ["SubscriptionRedeemCouponParams"]
+
+
+class SubscriptionRedeemCouponParams(TypedDict, total=False):
+ change_option: Required[Literal["requested_date", "end_of_subscription_term", "immediate"]]
+
+ coupon_id: Required[str]
+ """Coupon ID to be redeemed for this subscription."""
+
+ allow_invoice_credit_or_void: Optional[bool]
+ """
+ If false, this request will fail if it would void an issued invoice or create a
+ credit note. Consider using this as a safety mechanism if you do not expect
+ existing invoices to be changed.
+ """
+
+ change_date: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """The date that the coupon discount should take effect.
+
+ This parameter can only be passed if the `change_option` is `requested_date`.
+ """
diff --git a/src/orb/types/subscription_redeem_coupon_response.py b/src/orb/types/subscription_redeem_coupon_response.py
new file mode 100644
index 00000000..e0c0d5bd
--- /dev/null
+++ b/src/orb/types/subscription_redeem_coupon_response.py
@@ -0,0 +1,728 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, List, Union, Optional
+from datetime import datetime
+from typing_extensions import Literal, Annotated, TypeAlias
+
+from .plan import Plan
+from .price import Price
+from .._utils import PropertyInfo
+from .invoice import Invoice
+from .._models import BaseModel
+from .customer import Customer
+from .credit_note import CreditNote
+
+__all__ = [
+ "SubscriptionRedeemCouponResponse",
+ "AdjustmentInterval",
+ "AdjustmentIntervalAdjustment",
+ "AdjustmentIntervalAdjustmentPlanPhaseUsageDiscountAdjustment",
+ "AdjustmentIntervalAdjustmentPlanPhaseUsageDiscountAdjustmentFilter",
+ "AdjustmentIntervalAdjustmentPlanPhaseAmountDiscountAdjustment",
+ "AdjustmentIntervalAdjustmentPlanPhaseAmountDiscountAdjustmentFilter",
+ "AdjustmentIntervalAdjustmentPlanPhasePercentageDiscountAdjustment",
+ "AdjustmentIntervalAdjustmentPlanPhasePercentageDiscountAdjustmentFilter",
+ "AdjustmentIntervalAdjustmentPlanPhaseMinimumAdjustment",
+ "AdjustmentIntervalAdjustmentPlanPhaseMinimumAdjustmentFilter",
+ "AdjustmentIntervalAdjustmentPlanPhaseMaximumAdjustment",
+ "AdjustmentIntervalAdjustmentPlanPhaseMaximumAdjustmentFilter",
+ "BillingCycleAnchorConfiguration",
+ "DiscountInterval",
+ "DiscountIntervalAmountDiscountInterval",
+ "DiscountIntervalAmountDiscountIntervalFilter",
+ "DiscountIntervalPercentageDiscountInterval",
+ "DiscountIntervalPercentageDiscountIntervalFilter",
+ "DiscountIntervalUsageDiscountInterval",
+ "DiscountIntervalUsageDiscountIntervalFilter",
+ "FixedFeeQuantitySchedule",
+ "MaximumInterval",
+ "MaximumIntervalFilter",
+ "MinimumInterval",
+ "MinimumIntervalFilter",
+ "PendingSubscriptionChange",
+ "PriceInterval",
+ "PriceIntervalFixedFeeQuantityTransition",
+ "RedeemedCoupon",
+ "TrialInfo",
+ "ChangedResources",
+]
+
+
+class AdjustmentIntervalAdjustmentPlanPhaseUsageDiscountAdjustmentFilter(BaseModel):
+ field: Literal["price_id", "item_id", "price_type", "currency", "pricing_unit_id"]
+ """The property of the price to filter on."""
+
+ operator: Literal["includes", "excludes"]
+ """Should prices that match the filter be included or excluded."""
+
+ values: List[str]
+ """The IDs or values that match this filter."""
+
+
+class AdjustmentIntervalAdjustmentPlanPhaseUsageDiscountAdjustment(BaseModel):
+ id: str
+
+ adjustment_type: Literal["usage_discount"]
+
+ applies_to_price_ids: List[str]
+ """The price IDs that this adjustment applies to."""
+
+ filters: List[AdjustmentIntervalAdjustmentPlanPhaseUsageDiscountAdjustmentFilter]
+ """The filters that determine which prices to apply this adjustment to."""
+
+ is_invoice_level: bool
+ """
+ True for adjustments that apply to an entire invocice, false for adjustments
+ that apply to only one price.
+ """
+
+ plan_phase_order: Optional[int] = None
+ """The plan phase in which this adjustment is active."""
+
+ reason: Optional[str] = None
+ """The reason for the adjustment."""
+
+ usage_discount: float
+ """
+ The number of usage units by which to discount the price this adjustment applies
+ to in a given billing period.
+ """
+
+
+class AdjustmentIntervalAdjustmentPlanPhaseAmountDiscountAdjustmentFilter(BaseModel):
+ field: Literal["price_id", "item_id", "price_type", "currency", "pricing_unit_id"]
+ """The property of the price to filter on."""
+
+ operator: Literal["includes", "excludes"]
+ """Should prices that match the filter be included or excluded."""
+
+ values: List[str]
+ """The IDs or values that match this filter."""
+
+
+class AdjustmentIntervalAdjustmentPlanPhaseAmountDiscountAdjustment(BaseModel):
+ id: str
+
+ adjustment_type: Literal["amount_discount"]
+
+ amount_discount: str
+ """
+ The amount by which to discount the prices this adjustment applies to in a given
+ billing period.
+ """
+
+ applies_to_price_ids: List[str]
+ """The price IDs that this adjustment applies to."""
+
+ filters: List[AdjustmentIntervalAdjustmentPlanPhaseAmountDiscountAdjustmentFilter]
+ """The filters that determine which prices to apply this adjustment to."""
+
+ is_invoice_level: bool
+ """
+ True for adjustments that apply to an entire invocice, false for adjustments
+ that apply to only one price.
+ """
+
+ plan_phase_order: Optional[int] = None
+ """The plan phase in which this adjustment is active."""
+
+ reason: Optional[str] = None
+ """The reason for the adjustment."""
+
+
+class AdjustmentIntervalAdjustmentPlanPhasePercentageDiscountAdjustmentFilter(BaseModel):
+ field: Literal["price_id", "item_id", "price_type", "currency", "pricing_unit_id"]
+ """The property of the price to filter on."""
+
+ operator: Literal["includes", "excludes"]
+ """Should prices that match the filter be included or excluded."""
+
+ values: List[str]
+ """The IDs or values that match this filter."""
+
+
+class AdjustmentIntervalAdjustmentPlanPhasePercentageDiscountAdjustment(BaseModel):
+ id: str
+
+ adjustment_type: Literal["percentage_discount"]
+
+ applies_to_price_ids: List[str]
+ """The price IDs that this adjustment applies to."""
+
+ filters: List[AdjustmentIntervalAdjustmentPlanPhasePercentageDiscountAdjustmentFilter]
+ """The filters that determine which prices to apply this adjustment to."""
+
+ is_invoice_level: bool
+ """
+ True for adjustments that apply to an entire invocice, false for adjustments
+ that apply to only one price.
+ """
+
+ percentage_discount: float
+ """
+ The percentage (as a value between 0 and 1) by which to discount the price
+ intervals this adjustment applies to in a given billing period.
+ """
+
+ plan_phase_order: Optional[int] = None
+ """The plan phase in which this adjustment is active."""
+
+ reason: Optional[str] = None
+ """The reason for the adjustment."""
+
+
+class AdjustmentIntervalAdjustmentPlanPhaseMinimumAdjustmentFilter(BaseModel):
+ field: Literal["price_id", "item_id", "price_type", "currency", "pricing_unit_id"]
+ """The property of the price to filter on."""
+
+ operator: Literal["includes", "excludes"]
+ """Should prices that match the filter be included or excluded."""
+
+ values: List[str]
+ """The IDs or values that match this filter."""
+
+
+class AdjustmentIntervalAdjustmentPlanPhaseMinimumAdjustment(BaseModel):
+ id: str
+
+ adjustment_type: Literal["minimum"]
+
+ applies_to_price_ids: List[str]
+ """The price IDs that this adjustment applies to."""
+
+ filters: List[AdjustmentIntervalAdjustmentPlanPhaseMinimumAdjustmentFilter]
+ """The filters that determine which prices to apply this adjustment to."""
+
+ is_invoice_level: bool
+ """
+ True for adjustments that apply to an entire invocice, false for adjustments
+ that apply to only one price.
+ """
+
+ item_id: str
+ """The item ID that revenue from this minimum will be attributed to."""
+
+ minimum_amount: str
+ """
+ The minimum amount to charge in a given billing period for the prices this
+ adjustment applies to.
+ """
+
+ plan_phase_order: Optional[int] = None
+ """The plan phase in which this adjustment is active."""
+
+ reason: Optional[str] = None
+ """The reason for the adjustment."""
+
+
+class AdjustmentIntervalAdjustmentPlanPhaseMaximumAdjustmentFilter(BaseModel):
+ field: Literal["price_id", "item_id", "price_type", "currency", "pricing_unit_id"]
+ """The property of the price to filter on."""
+
+ operator: Literal["includes", "excludes"]
+ """Should prices that match the filter be included or excluded."""
+
+ values: List[str]
+ """The IDs or values that match this filter."""
+
+
+class AdjustmentIntervalAdjustmentPlanPhaseMaximumAdjustment(BaseModel):
+ id: str
+
+ adjustment_type: Literal["maximum"]
+
+ applies_to_price_ids: List[str]
+ """The price IDs that this adjustment applies to."""
+
+ filters: List[AdjustmentIntervalAdjustmentPlanPhaseMaximumAdjustmentFilter]
+ """The filters that determine which prices to apply this adjustment to."""
+
+ is_invoice_level: bool
+ """
+ True for adjustments that apply to an entire invocice, false for adjustments
+ that apply to only one price.
+ """
+
+ maximum_amount: str
+ """
+ The maximum amount to charge in a given billing period for the prices this
+ adjustment applies to.
+ """
+
+ plan_phase_order: Optional[int] = None
+ """The plan phase in which this adjustment is active."""
+
+ reason: Optional[str] = None
+ """The reason for the adjustment."""
+
+
+AdjustmentIntervalAdjustment: TypeAlias = Annotated[
+ Union[
+ AdjustmentIntervalAdjustmentPlanPhaseUsageDiscountAdjustment,
+ AdjustmentIntervalAdjustmentPlanPhaseAmountDiscountAdjustment,
+ AdjustmentIntervalAdjustmentPlanPhasePercentageDiscountAdjustment,
+ AdjustmentIntervalAdjustmentPlanPhaseMinimumAdjustment,
+ AdjustmentIntervalAdjustmentPlanPhaseMaximumAdjustment,
+ ],
+ PropertyInfo(discriminator="adjustment_type"),
+]
+
+
+class AdjustmentInterval(BaseModel):
+ id: str
+
+ adjustment: AdjustmentIntervalAdjustment
+
+ applies_to_price_interval_ids: List[str]
+ """The price interval IDs that this adjustment applies to."""
+
+ end_date: Optional[datetime] = None
+ """The end date of the adjustment interval."""
+
+ start_date: datetime
+ """The start date of the adjustment interval."""
+
+
+class BillingCycleAnchorConfiguration(BaseModel):
+ day: int
+ """The day of the month on which the billing cycle is anchored.
+
+ If the maximum number of days in a month is greater than this value, the last
+ day of the month is the billing cycle day (e.g. billing_cycle_day=31 for April
+ means the billing period begins on the 30th.
+ """
+
+ month: Optional[int] = None
+ """The month on which the billing cycle is anchored (e.g.
+
+ a quarterly price anchored in February would have cycles starting February, May,
+ August, and November).
+ """
+
+ year: Optional[int] = None
+ """The year on which the billing cycle is anchored (e.g.
+
+ a 2 year billing cycle anchored on 2021 would have cycles starting on 2021,
+ 2023, 2025, etc.).
+ """
+
+
+class DiscountIntervalAmountDiscountIntervalFilter(BaseModel):
+ field: Literal["price_id", "item_id", "price_type", "currency", "pricing_unit_id"]
+ """The property of the price to filter on."""
+
+ operator: Literal["includes", "excludes"]
+ """Should prices that match the filter be included or excluded."""
+
+ values: List[str]
+ """The IDs or values that match this filter."""
+
+
+class DiscountIntervalAmountDiscountInterval(BaseModel):
+ amount_discount: str
+ """Only available if discount_type is `amount`."""
+
+ applies_to_price_interval_ids: List[str]
+ """The price interval ids that this discount interval applies to."""
+
+ discount_type: Literal["amount"]
+
+ end_date: Optional[datetime] = None
+ """The end date of the discount interval."""
+
+ filters: List[DiscountIntervalAmountDiscountIntervalFilter]
+ """The filters that determine which prices this discount interval applies to."""
+
+ start_date: datetime
+ """The start date of the discount interval."""
+
+
+class DiscountIntervalPercentageDiscountIntervalFilter(BaseModel):
+ field: Literal["price_id", "item_id", "price_type", "currency", "pricing_unit_id"]
+ """The property of the price to filter on."""
+
+ operator: Literal["includes", "excludes"]
+ """Should prices that match the filter be included or excluded."""
+
+ values: List[str]
+ """The IDs or values that match this filter."""
+
+
+class DiscountIntervalPercentageDiscountInterval(BaseModel):
+ applies_to_price_interval_ids: List[str]
+ """The price interval ids that this discount interval applies to."""
+
+ discount_type: Literal["percentage"]
+
+ end_date: Optional[datetime] = None
+ """The end date of the discount interval."""
+
+ filters: List[DiscountIntervalPercentageDiscountIntervalFilter]
+ """The filters that determine which prices this discount interval applies to."""
+
+ percentage_discount: float
+ """
+ Only available if discount_type is `percentage`.This is a number between 0
+ and 1.
+ """
+
+ start_date: datetime
+ """The start date of the discount interval."""
+
+
+class DiscountIntervalUsageDiscountIntervalFilter(BaseModel):
+ field: Literal["price_id", "item_id", "price_type", "currency", "pricing_unit_id"]
+ """The property of the price to filter on."""
+
+ operator: Literal["includes", "excludes"]
+ """Should prices that match the filter be included or excluded."""
+
+ values: List[str]
+ """The IDs or values that match this filter."""
+
+
+class DiscountIntervalUsageDiscountInterval(BaseModel):
+ applies_to_price_interval_ids: List[str]
+ """The price interval ids that this discount interval applies to."""
+
+ discount_type: Literal["usage"]
+
+ end_date: Optional[datetime] = None
+ """The end date of the discount interval."""
+
+ filters: List[DiscountIntervalUsageDiscountIntervalFilter]
+ """The filters that determine which prices this discount interval applies to."""
+
+ start_date: datetime
+ """The start date of the discount interval."""
+
+ usage_discount: float
+ """Only available if discount_type is `usage`.
+
+ Number of usage units that this discount is for
+ """
+
+
+DiscountInterval: TypeAlias = Annotated[
+ Union[
+ DiscountIntervalAmountDiscountInterval,
+ DiscountIntervalPercentageDiscountInterval,
+ DiscountIntervalUsageDiscountInterval,
+ ],
+ PropertyInfo(discriminator="discount_type"),
+]
+
+
+class FixedFeeQuantitySchedule(BaseModel):
+ end_date: Optional[datetime] = None
+
+ price_id: str
+
+ quantity: float
+
+ start_date: datetime
+
+
+class MaximumIntervalFilter(BaseModel):
+ field: Literal["price_id", "item_id", "price_type", "currency", "pricing_unit_id"]
+ """The property of the price to filter on."""
+
+ operator: Literal["includes", "excludes"]
+ """Should prices that match the filter be included or excluded."""
+
+ values: List[str]
+ """The IDs or values that match this filter."""
+
+
+class MaximumInterval(BaseModel):
+ applies_to_price_interval_ids: List[str]
+ """The price interval ids that this maximum interval applies to."""
+
+ end_date: Optional[datetime] = None
+ """The end date of the maximum interval."""
+
+ filters: List[MaximumIntervalFilter]
+ """The filters that determine which prices this maximum interval applies to."""
+
+ maximum_amount: str
+ """
+ The maximum amount to charge in a given billing period for the price intervals
+ this transform applies to.
+ """
+
+ start_date: datetime
+ """The start date of the maximum interval."""
+
+
+class MinimumIntervalFilter(BaseModel):
+ field: Literal["price_id", "item_id", "price_type", "currency", "pricing_unit_id"]
+ """The property of the price to filter on."""
+
+ operator: Literal["includes", "excludes"]
+ """Should prices that match the filter be included or excluded."""
+
+ values: List[str]
+ """The IDs or values that match this filter."""
+
+
+class MinimumInterval(BaseModel):
+ applies_to_price_interval_ids: List[str]
+ """The price interval ids that this minimum interval applies to."""
+
+ end_date: Optional[datetime] = None
+ """The end date of the minimum interval."""
+
+ filters: List[MinimumIntervalFilter]
+ """The filters that determine which prices this minimum interval applies to."""
+
+ minimum_amount: str
+ """
+ The minimum amount to charge in a given billing period for the price intervals
+ this minimum applies to.
+ """
+
+ start_date: datetime
+ """The start date of the minimum interval."""
+
+
+class PendingSubscriptionChange(BaseModel):
+ id: str
+
+
+class PriceIntervalFixedFeeQuantityTransition(BaseModel):
+ effective_date: datetime
+
+ price_id: str
+
+ quantity: int
+
+
+class PriceInterval(BaseModel):
+ id: str
+
+ billing_cycle_day: int
+ """The day of the month that Orb bills for this price"""
+
+ current_billing_period_end_date: Optional[datetime] = None
+ """The end of the current billing period.
+
+ This is an exclusive timestamp, such that the instant returned is exactly the
+ end of the billing period. Set to null if this price interval is not currently
+ active.
+ """
+
+ current_billing_period_start_date: Optional[datetime] = None
+ """The start date of the current billing period.
+
+ This is an inclusive timestamp; the instant returned is exactly the beginning of
+ the billing period. Set to null if this price interval is not currently active.
+ """
+
+ end_date: Optional[datetime] = None
+ """The end date of the price interval.
+
+ This is the date that Orb stops billing for this price.
+ """
+
+ filter: Optional[str] = None
+ """An additional filter to apply to usage queries."""
+
+ fixed_fee_quantity_transitions: Optional[List[PriceIntervalFixedFeeQuantityTransition]] = None
+ """The fixed fee quantity transitions for this price interval.
+
+ This is only relevant for fixed fees.
+ """
+
+ price: Price
+ """
+ The Price resource represents a price that can be billed on a subscription,
+ resulting in a charge on an invoice in the form of an invoice line item. Prices
+ take a quantity and determine an amount to bill.
+
+ Orb supports a few different pricing models out of the box. Each of these models
+ is serialized differently in a given Price object. The model_type field
+ determines the key for the configuration object that is present.
+
+ For more on the types of prices, see
+ [the core concepts documentation](/core-concepts#plan-and-price)
+ """
+
+ start_date: datetime
+ """The start date of the price interval.
+
+ This is the date that Orb starts billing for this price.
+ """
+
+ usage_customer_ids: Optional[List[str]] = None
+ """
+ A list of customer IDs whose usage events will be aggregated and billed under
+ this price interval.
+ """
+
+
+class RedeemedCoupon(BaseModel):
+ coupon_id: str
+
+ end_date: Optional[datetime] = None
+
+ start_date: datetime
+
+
+class TrialInfo(BaseModel):
+ end_date: Optional[datetime] = None
+
+
+class ChangedResources(BaseModel):
+ created_credit_notes: List[CreditNote]
+ """The credit notes that were created as part of this operation."""
+
+ created_invoices: List[Invoice]
+ """The invoices that were created as part of this operation."""
+
+ voided_credit_notes: List[CreditNote]
+ """The credit notes that were voided as part of this operation."""
+
+ voided_invoices: List[Invoice]
+ """The invoices that were voided as part of this operation."""
+
+
+class SubscriptionRedeemCouponResponse(BaseModel):
+ id: str
+
+ active_plan_phase_order: Optional[int] = None
+ """
+ The current plan phase that is active, only if the subscription's plan has
+ phases.
+ """
+
+ adjustment_intervals: List[AdjustmentInterval]
+ """
+ The adjustment intervals for this subscription sorted by the start_date of the
+ adjustment interval.
+ """
+
+ auto_collection: Optional[bool] = None
+ """
+ Determines whether issued invoices for this subscription will automatically be
+ charged with the saved payment method on the due date. This property defaults to
+ the plan's behavior. If null, defaults to the customer's setting.
+ """
+
+ billing_cycle_anchor_configuration: BillingCycleAnchorConfiguration
+
+ billing_cycle_day: int
+ """The day of the month on which the billing cycle is anchored.
+
+ If the maximum number of days in a month is greater than this value, the last
+ day of the month is the billing cycle day (e.g. billing_cycle_day=31 for April
+ means the billing period begins on the 30th.
+ """
+
+ created_at: datetime
+
+ current_billing_period_end_date: Optional[datetime] = None
+ """The end of the current billing period.
+
+ This is an exclusive timestamp, such that the instant returned is not part of
+ the billing period. Set to null for subscriptions that are not currently active.
+ """
+
+ current_billing_period_start_date: Optional[datetime] = None
+ """The start date of the current billing period.
+
+ This is an inclusive timestamp; the instant returned is exactly the beginning of
+ the billing period. Set to null if the subscription is not currently active.
+ """
+
+ customer: Customer
+ """
+ A customer is a buyer of your products, and the other party to the billing
+ relationship.
+
+ In Orb, customers are assigned system generated identifiers automatically, but
+ it's often desirable to have these match existing identifiers in your system. To
+ avoid having to denormalize Orb ID information, you can pass in an
+ `external_customer_id` with your own identifier. See
+ [Customer ID Aliases](/events-and-metrics/customer-aliases) for further
+ information about how these aliases work in Orb.
+
+ In addition to having an identifier in your system, a customer may exist in a
+ payment provider solution like Stripe. Use the `payment_provider_id` and the
+ `payment_provider` enum field to express this mapping.
+
+ A customer also has a timezone (from the standard
+ [IANA timezone database](https://www.iana.org/time-zones)), which defaults to
+ your account's timezone. See [Timezone localization](/essentials/timezones) for
+ information on what this timezone parameter influences within Orb.
+ """
+
+ default_invoice_memo: Optional[str] = None
+ """Determines the default memo on this subscriptions' invoices.
+
+ Note that if this is not provided, it is determined by the plan configuration.
+ """
+
+ discount_intervals: List[DiscountInterval]
+ """The discount intervals for this subscription sorted by the start_date."""
+
+ end_date: Optional[datetime] = None
+ """The date Orb stops billing for this subscription."""
+
+ fixed_fee_quantity_schedule: List[FixedFeeQuantitySchedule]
+
+ invoicing_threshold: Optional[str] = None
+
+ maximum_intervals: List[MaximumInterval]
+ """The maximum intervals for this subscription sorted by the start_date."""
+
+ metadata: Dict[str, str]
+ """User specified key-value pairs for the resource.
+
+ If not present, this defaults to an empty dictionary. Individual keys can be
+ removed by setting the value to `null`, and the entire metadata mapping can be
+ cleared by setting `metadata` to `null`.
+ """
+
+ minimum_intervals: List[MinimumInterval]
+ """The minimum intervals for this subscription sorted by the start_date."""
+
+ name: str
+ """The name of the subscription."""
+
+ net_terms: int
+ """
+ Determines the difference between the invoice issue date for subscription
+ invoices as the date that they are due. A value of `0` here represents that the
+ invoice is due on issue, whereas a value of `30` represents that the customer
+ has a month to pay the invoice.
+ """
+
+ pending_subscription_change: Optional[PendingSubscriptionChange] = None
+ """A pending subscription change if one exists on this subscription."""
+
+ plan: Optional[Plan] = None
+ """
+ The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be
+ subscribed to by a customer. Plans define the billing behavior of the
+ subscription. You can see more about how to configure prices in the
+ [Price resource](/reference/price).
+ """
+
+ price_intervals: List[PriceInterval]
+ """The price intervals for this subscription."""
+
+ redeemed_coupon: Optional[RedeemedCoupon] = None
+
+ start_date: datetime
+ """The date Orb starts billing for this subscription."""
+
+ status: Literal["active", "ended", "upcoming"]
+
+ trial_info: TrialInfo
+
+ changed_resources: Optional[ChangedResources] = None
+ """The resources that were changed as part of this operation.
+
+ Only present when fetched through the subscription changes API or if the
+ `include_changed_resources` parameter was passed in the request.
+ """
diff --git a/tests/api_resources/test_subscriptions.py b/tests/api_resources/test_subscriptions.py
index d6d46997..08ad18e5 100644
--- a/tests/api_resources/test_subscriptions.py
+++ b/tests/api_resources/test_subscriptions.py
@@ -15,6 +15,7 @@
SubscriptionCreateResponse,
SubscriptionFetchCostsResponse,
SubscriptionUpdateTrialResponse,
+ SubscriptionRedeemCouponResponse,
SubscriptionTriggerPhaseResponse,
SubscriptionFetchScheduleResponse,
SubscriptionPriceIntervalsResponse,
@@ -730,6 +731,63 @@ def test_path_params_price_intervals(self, client: Orb) -> None:
subscription_id="",
)
+ @parametrize
+ def test_method_redeem_coupon(self, client: Orb) -> None:
+ subscription = client.subscriptions.redeem_coupon(
+ subscription_id="subscription_id",
+ change_option="requested_date",
+ coupon_id="coupon_id",
+ )
+ assert_matches_type(SubscriptionRedeemCouponResponse, subscription, path=["response"])
+
+ @parametrize
+ def test_method_redeem_coupon_with_all_params(self, client: Orb) -> None:
+ subscription = client.subscriptions.redeem_coupon(
+ subscription_id="subscription_id",
+ change_option="requested_date",
+ coupon_id="coupon_id",
+ allow_invoice_credit_or_void=True,
+ change_date=parse_datetime("2017-07-21T17:32:28Z"),
+ )
+ assert_matches_type(SubscriptionRedeemCouponResponse, subscription, path=["response"])
+
+ @parametrize
+ def test_raw_response_redeem_coupon(self, client: Orb) -> None:
+ response = client.subscriptions.with_raw_response.redeem_coupon(
+ subscription_id="subscription_id",
+ change_option="requested_date",
+ coupon_id="coupon_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ subscription = response.parse()
+ assert_matches_type(SubscriptionRedeemCouponResponse, subscription, path=["response"])
+
+ @parametrize
+ def test_streaming_response_redeem_coupon(self, client: Orb) -> None:
+ with client.subscriptions.with_streaming_response.redeem_coupon(
+ subscription_id="subscription_id",
+ change_option="requested_date",
+ coupon_id="coupon_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ subscription = response.parse()
+ assert_matches_type(SubscriptionRedeemCouponResponse, subscription, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_redeem_coupon(self, client: Orb) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `subscription_id` but received ''"):
+ client.subscriptions.with_raw_response.redeem_coupon(
+ subscription_id="",
+ change_option="requested_date",
+ coupon_id="coupon_id",
+ )
+
@parametrize
def test_method_schedule_plan_change(self, client: Orb) -> None:
subscription = client.subscriptions.schedule_plan_change(
@@ -1923,6 +1981,63 @@ async def test_path_params_price_intervals(self, async_client: AsyncOrb) -> None
subscription_id="",
)
+ @parametrize
+ async def test_method_redeem_coupon(self, async_client: AsyncOrb) -> None:
+ subscription = await async_client.subscriptions.redeem_coupon(
+ subscription_id="subscription_id",
+ change_option="requested_date",
+ coupon_id="coupon_id",
+ )
+ assert_matches_type(SubscriptionRedeemCouponResponse, subscription, path=["response"])
+
+ @parametrize
+ async def test_method_redeem_coupon_with_all_params(self, async_client: AsyncOrb) -> None:
+ subscription = await async_client.subscriptions.redeem_coupon(
+ subscription_id="subscription_id",
+ change_option="requested_date",
+ coupon_id="coupon_id",
+ allow_invoice_credit_or_void=True,
+ change_date=parse_datetime("2017-07-21T17:32:28Z"),
+ )
+ assert_matches_type(SubscriptionRedeemCouponResponse, subscription, path=["response"])
+
+ @parametrize
+ async def test_raw_response_redeem_coupon(self, async_client: AsyncOrb) -> None:
+ response = await async_client.subscriptions.with_raw_response.redeem_coupon(
+ subscription_id="subscription_id",
+ change_option="requested_date",
+ coupon_id="coupon_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ subscription = response.parse()
+ assert_matches_type(SubscriptionRedeemCouponResponse, subscription, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_redeem_coupon(self, async_client: AsyncOrb) -> None:
+ async with async_client.subscriptions.with_streaming_response.redeem_coupon(
+ subscription_id="subscription_id",
+ change_option="requested_date",
+ coupon_id="coupon_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ subscription = await response.parse()
+ assert_matches_type(SubscriptionRedeemCouponResponse, subscription, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_redeem_coupon(self, async_client: AsyncOrb) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `subscription_id` but received ''"):
+ await async_client.subscriptions.with_raw_response.redeem_coupon(
+ subscription_id="",
+ change_option="requested_date",
+ coupon_id="coupon_id",
+ )
+
@parametrize
async def test_method_schedule_plan_change(self, async_client: AsyncOrb) -> None:
subscription = await async_client.subscriptions.schedule_plan_change(
diff --git a/tests/test_client.py b/tests/test_client.py
index 41b2ec55..88928ef0 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -869,6 +869,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
assert response.retries_taken == failures_before_success
assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
+ @pytest.mark.respx(base_url=base_url)
+ def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ # Test that the default follow_redirects=True allows following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+ respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
+
+ response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok"}
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ # Test that follow_redirects=False prevents following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+
+ with pytest.raises(APIStatusError) as exc_info:
+ self.client.post(
+ "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
+ )
+
+ assert exc_info.value.response.status_code == 302
+ assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
+
class TestAsyncOrb:
client = AsyncOrb(base_url=base_url, api_key=api_key, _strict_response_validation=True)
@@ -1751,3 +1778,30 @@ async def test_main() -> None:
raise AssertionError("calling get_platform using asyncify resulted in a hung process")
time.sleep(0.1)
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ # Test that the default follow_redirects=True allows following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+ respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
+
+ response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok"}
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ # Test that follow_redirects=False prevents following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+
+ with pytest.raises(APIStatusError) as exc_info:
+ await self.client.post(
+ "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
+ )
+
+ assert exc_info.value.response.status_code == 302
+ assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"