diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index abcfa5672..a0e9c8dc7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -63,7 +63,7 @@ jobs:
- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/metronome-python'
id: github-oidc
- uses: actions/github-script@v6
+ uses: actions/github-script@v8
with:
script: core.setOutput('github_token', await core.getIDToken());
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 127ac87b5..3b4c2d4ba 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "4.0.0"
+ ".": "4.1.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index c37cb3c12..84053d28e 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 110
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/metronome%2Fmetronome-e849724f8555bd9026b46bc4f8f3071dfe91e2bcdb5d3c26fa2faad85feda489.yml
-openapi_spec_hash: bf4df8680beb0c3b66b3424d5e1c2e23
-config_hash: 5aa28b992429a1dddfb3c5b0d777d1ce
+configured_endpoints: 111
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/metronome%2Fmetronome-a8bac472f2649f1084acbf3bbc92b0f05741e7d85bbb835b6ff7268b9d88156f.yml
+openapi_spec_hash: 12c5c4d38046cc0c80d59048b412a4b8
+config_hash: a5826161026c5fb08e8fafaba6607893
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9cab8f16..c7410c7f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,34 @@
# Changelog
+## 4.1.0 (2026-02-04)
+
+Full Changelog: [v4.0.0...v4.1.0](https://github.com/Metronome-Industries/metronome-python/compare/v4.0.0...v4.1.0)
+
+### Features
+
+* Add `exclude` field to commit and override specifiers shape in the API ([924202e](https://github.com/Metronome-Industries/metronome-python/commit/924202e951e65f67691402d3e5b3556740328da3))
+* **api:** Add getNetBalance endpoint ([d1335f9](https://github.com/Metronome-Industries/metronome-python/commit/d1335f9617bf64f8be39c16d882bbdb91089db1e))
+* bump schema version and update code to support TieredPercentage rate ([9b583a2](https://github.com/Metronome-Industries/metronome-python/commit/9b583a264e2d703b700a4c216e991a8ed42ddb73))
+* **client:** add custom JSON encoder for extended type support ([a76020d](https://github.com/Metronome-Industries/metronome-python/commit/a76020d5569fe1ec31bcf2c79ed7e69f8c7662eb))
+* Expose add revenue config in edit contract api ([5422277](https://github.com/Metronome-Industries/metronome-python/commit/5422277dc61e40efe4c0f47d7f8e2a238ecdcad4))
+* release account hierarchy ([656b37c](https://github.com/Metronome-Industries/metronome-python/commit/656b37c265cd5c7b7fa0a60f78660dce15a7a6a2))
+* update api to allow editing net payment terms ([dd4c885](https://github.com/Metronome-Industries/metronome-python/commit/dd4c88549c93cf369b57c97029a073a9b7b3cf3c))
+
+
+### Bug Fixes
+
+* **docs:** fix mcp installation instructions for remote servers ([5b446b6](https://github.com/Metronome-Industries/metronome-python/commit/5b446b6f3459edb8ceadad163e3a0f2906b2ee4a))
+
+
+### Chores
+
+* **ci:** upgrade `actions/github-script` ([289b5eb](https://github.com/Metronome-Industries/metronome-python/commit/289b5ebdbdff707f1080af12d9bdd2f9621408e4))
+
+
+### Documentation
+
+* Update package docs for GA ([83d58c1](https://github.com/Metronome-Industries/metronome-python/commit/83d58c14e4e48a6bf66bec5c0c833739331122dc))
+
## 4.0.0 (2026-01-17)
Full Changelog: [v3.2.0...v4.0.0](https://github.com/Metronome-Industries/metronome-python/compare/v3.2.0...v4.0.0)
diff --git a/README.md b/README.md
index 00d3dd229..15d6e9f53 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,8 @@ and offers both synchronous and asynchronous clients powered by [httpx](https://
Use the Metronome MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.
-[](https://cursor.com/en-US/install-mcp?name=%40metronome%2Fmcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBtZXRyb25vbWUvbWNwIl19)
-[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40metronome%2Fmcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40metronome%2Fmcp%22%5D%7D)
+[](https://cursor.com/en-US/install-mcp?name=%40metronome%2Fmcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBtZXRyb25vbWUvbWNwIl0sImVudiI6eyJNRVRST05PTUVfQkVBUkVSX1RPS0VOIjoiTXkgQmVhcmVyIFRva2VuIiwiTUVUUk9OT01FX1dFQkhPT0tfU0VDUkVUIjoiTXkgV2ViaG9vayBTZWNyZXQifX0)
+[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40metronome%2Fmcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40metronome%2Fmcp%22%5D%2C%22env%22%3A%7B%22METRONOME_BEARER_TOKEN%22%3A%22My%20Bearer%20Token%22%2C%22METRONOME_WEBHOOK_SECRET%22%3A%22My%20Webhook%20Secret%22%7D%7D)
> Note: You may need to set environment variables in your MCP client.
diff --git a/api.md b/api.md
index 93c1dc67d..a4fcb5b44 100644
--- a/api.md
+++ b/api.md
@@ -2,6 +2,7 @@
```python
from metronome.types import (
+ BalanceFilter,
BaseThresholdCommit,
BaseUsageFilter,
Commit,
@@ -396,6 +397,7 @@ from metronome.types.v1 import (
ContractAmendResponse,
ContractArchiveResponse,
ContractCreateHistoricalInvoicesResponse,
+ ContractGetNetBalanceResponse,
ContractListBalancesResponse,
ContractRetrieveRateScheduleResponse,
ContractRetrieveSubscriptionQuantityHistoryResponse,
@@ -413,6 +415,7 @@ Methods:
- client.v1.contracts.amend(\*\*params) -> ContractAmendResponse
- client.v1.contracts.archive(\*\*params) -> ContractArchiveResponse
- client.v1.contracts.create_historical_invoices(\*\*params) -> ContractCreateHistoricalInvoicesResponse
+- client.v1.contracts.get_net_balance(\*\*params) -> ContractGetNetBalanceResponse
- client.v1.contracts.list_balances(\*\*params) -> SyncBodyCursorPage[ContractListBalancesResponse]
- client.v1.contracts.retrieve_rate_schedule(\*\*params) -> ContractRetrieveRateScheduleResponse
- client.v1.contracts.retrieve_subscription_quantity_history(\*\*params) -> ContractRetrieveSubscriptionQuantityHistoryResponse
diff --git a/pyproject.toml b/pyproject.toml
index f1fa82c73..4a7c4762e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "metronome-sdk"
-version = "4.0.0"
+version = "4.1.0"
description = "The official Python library for the metronome API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/metronome/_base_client.py b/src/metronome/_base_client.py
index 5b2535707..aeee5394d 100644
--- a/src/metronome/_base_client.py
+++ b/src/metronome/_base_client.py
@@ -86,6 +86,7 @@
APIConnectionError,
APIResponseValidationError,
)
+from ._utils._json import openapi_dumps
log: logging.Logger = logging.getLogger(__name__)
@@ -554,8 +555,10 @@ def _build_request(
kwargs["content"] = options.content
elif isinstance(json_data, bytes):
kwargs["content"] = json_data
- else:
- kwargs["json"] = json_data if is_given(json_data) else None
+ elif not files:
+ # Don't set content when JSON is sent as multipart/form-data,
+ # since httpx's content param overrides other body arguments
+ kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
kwargs["files"] = files
else:
headers.pop("Content-Type", None)
diff --git a/src/metronome/_compat.py b/src/metronome/_compat.py
index bdef67f04..786ff42ad 100644
--- a/src/metronome/_compat.py
+++ b/src/metronome/_compat.py
@@ -139,6 +139,7 @@ def model_dump(
exclude_defaults: bool = False,
warnings: bool = True,
mode: Literal["json", "python"] = "python",
+ by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
return model.model_dump(
@@ -148,13 +149,12 @@ def model_dump(
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
+ by_alias=by_alias,
)
return cast(
"dict[str, Any]",
model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
- exclude=exclude,
- exclude_unset=exclude_unset,
- exclude_defaults=exclude_defaults,
+ exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
),
)
diff --git a/src/metronome/_utils/_json.py b/src/metronome/_utils/_json.py
new file mode 100644
index 000000000..60584214a
--- /dev/null
+++ b/src/metronome/_utils/_json.py
@@ -0,0 +1,35 @@
+import json
+from typing import Any
+from datetime import datetime
+from typing_extensions import override
+
+import pydantic
+
+from .._compat import model_dump
+
+
+def openapi_dumps(obj: Any) -> bytes:
+ """
+ Serialize an object to UTF-8 encoded JSON bytes.
+
+ Extends the standard json.dumps with support for additional types
+ commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
+ """
+ return json.dumps(
+ obj,
+ cls=_CustomEncoder,
+ # Uses the same defaults as httpx's JSON serialization
+ ensure_ascii=False,
+ separators=(",", ":"),
+ allow_nan=False,
+ ).encode()
+
+
+class _CustomEncoder(json.JSONEncoder):
+ @override
+ def default(self, o: Any) -> Any:
+ if isinstance(o, datetime):
+ return o.isoformat()
+ if isinstance(o, pydantic.BaseModel):
+ return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
+ return super().default(o)
diff --git a/src/metronome/_version.py b/src/metronome/_version.py
index 52fc1a2c5..159232e03 100644
--- a/src/metronome/_version.py
+++ b/src/metronome/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "metronome"
-__version__ = "4.0.0" # x-release-please-version
+__version__ = "4.1.0" # x-release-please-version
diff --git a/src/metronome/resources/v1/contracts/contracts.py b/src/metronome/resources/v1/contracts/contracts.py
index ff8955944..59d9300b5 100644
--- a/src/metronome/resources/v1/contracts/contracts.py
+++ b/src/metronome/resources/v1/contracts/contracts.py
@@ -26,6 +26,7 @@
contract_archive_params,
contract_retrieve_params,
contract_list_balances_params,
+ contract_get_net_balance_params,
contract_update_end_date_params,
contract_set_usage_filter_params,
contract_retrieve_rate_schedule_params,
@@ -62,10 +63,12 @@
from ....types.v1.contract_list_response import ContractListResponse
from ....types.v1.contract_amend_response import ContractAmendResponse
from ....types.v1.contract_create_response import ContractCreateResponse
+from ....types.shared_params.balance_filter import BalanceFilter
from ....types.v1.contract_archive_response import ContractArchiveResponse
from ....types.v1.contract_retrieve_response import ContractRetrieveResponse
from ....types.shared_params.base_usage_filter import BaseUsageFilter
from ....types.v1.contract_list_balances_response import ContractListBalancesResponse
+from ....types.v1.contract_get_net_balance_response import ContractGetNetBalanceResponse
from ....types.v1.contract_update_end_date_response import ContractUpdateEndDateResponse
from ....types.shared_params.spend_threshold_configuration import SpendThresholdConfiguration
from ....types.v1.contract_retrieve_rate_schedule_response import ContractRetrieveRateScheduleResponse
@@ -128,6 +131,8 @@ def create(
net_payment_terms_days: float | Omit = omit,
netsuite_sales_order_id: str | Omit = omit,
overrides: Iterable[contract_create_params.Override] | Omit = omit,
+ package_alias: str | Omit = omit,
+ package_id: str | Omit = omit,
prepaid_balance_threshold_configuration: PrepaidBalanceThresholdConfiguration | Omit = omit,
priority: float | Omit = omit,
professional_services: Iterable[contract_create_params.ProfessionalService] | Omit = omit,
@@ -298,6 +303,13 @@ def create(
netsuite_sales_order_id: This field's availability is dependent on your client's configuration.
+ package_alias: Selects the package linked to the specified alias as of the contract's start
+ date. Mutually exclusive with package_id.
+
+ package_id: If provided, provisions a customer on a package instead of creating a
+ traditional contract. When specified, only customer_id, starting_at, package_id,
+ and uniqueness_key are allowed.
+
priority: Priority of the contract.
professional_services: This field's availability is dependent on your client's configuration.
@@ -354,6 +366,8 @@ def create(
"net_payment_terms_days": net_payment_terms_days,
"netsuite_sales_order_id": netsuite_sales_order_id,
"overrides": overrides,
+ "package_alias": package_alias,
+ "package_id": package_id,
"prepaid_balance_threshold_configuration": prepaid_balance_threshold_configuration,
"priority": priority,
"professional_services": professional_services,
@@ -789,6 +803,106 @@ def create_historical_invoices(
cast_to=ContractCreateHistoricalInvoicesResponse,
)
+ def get_net_balance(
+ self,
+ *,
+ customer_id: str,
+ credit_type_id: str | Omit = omit,
+ filters: Iterable[BalanceFilter] | Omit = omit,
+ invoice_inclusion_mode: Literal["FINALIZED", "FINALIZED_AND_DRAFT"] | Omit = omit,
+ # 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,
+ ) -> ContractGetNetBalanceResponse:
+ """
+ Retrieve the combined current balance across any grouping of credits and commits
+ for a customer in a single API call.
+
+ - Display real-time available balance to customers in billing dashboards
+ - Build finance dashboards showing credit utilization across customer segments
+ - Validate expected vs. actual balance during billing reconciliation
+
+ ### Key response fields:
+
+ - `balance`: The combined net balance available to use at this moment across all
+ matching commits and credits
+ - `credit_type_id`: The credit type (fiat or custom pricing unit) the balance is
+ denominated in
+
+ ### Filtering options:
+
+ Balance filters allow you to scope the calculation to specific subsets of
+ commits and credits. When using multiple filter objects, they are OR'd together
+ — if a commit or credit matches any filter, it's included in the net balance.
+ Within a single filter object, all specified conditions are AND'd together.
+
+ - **Balance types**: Include any combination of `PREPAID_COMMIT`,
+ `POSTPAID_COMMIT`, and `CREDIT` (e.g., `["PREPAID_COMMIT", "CREDIT"]` to
+ exclude postpaid commits). If not specified, all balance types are included.
+ - **Specific IDs**: Target exact commit or credit IDs for precise balance
+ queries
+ - **Custom fields**: Filter by custom field key-value pairs; when multiple pairs
+ are provided, commits must match all of them
+
+ **Example**: To get the balance of all free-trial credits OR all
+ signup-promotion commits, you'd pass two filter objects — one filtering for
+ CREDIT with custom field campaign: free-trial, and another filtering for
+ PREPAID_COMMIT with custom field campaign: signup-promotion.
+
+ ### Usage guidelines:
+
+ - **Draft invoice handling**: Use `invoice_inclusion_mode` to control whether
+ pending draft invoice deductions are included (`FINALIZED_AND_DRAFT`, the
+ default) or excluded (`FINALIZED`) from the balance calculation
+ - **Account hierarchies**: When querying a child customer, shared commits from
+ parent contracts are not included — query the parent customer directly to see
+ shared commit balances
+ - **Negative balances**: Manual ledger entries can cause negative segment
+ balances; these are treated as zero when calculating the net balance
+ - **Credit types**: If `credit_type_id` is not specified, the balance defaults
+ to USD (cents)
+
+ Args:
+ customer_id: The ID of the customer.
+
+ credit_type_id: The ID of the credit type (can be fiat or a custom pricing unit) to get the
+ balance for. Defaults to USD (cents) if not specified.
+
+ filters: Balance filters are OR'd together, so if a given commit or credit matches any of
+ the filters, it will be included in the net balance.
+
+ invoice_inclusion_mode: Controls which invoices are considered when calculating the remaining balance.
+ `FINALIZED` considers only deductions from finalized invoices.
+ `FINALIZED_AND_DRAFT` also includes deductions from pending draft invoices.
+
+ 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
+ """
+ return self._post(
+ "/v1/contracts/customerBalances/getNetBalance",
+ body=maybe_transform(
+ {
+ "customer_id": customer_id,
+ "credit_type_id": credit_type_id,
+ "filters": filters,
+ "invoice_inclusion_mode": invoice_inclusion_mode,
+ },
+ contract_get_net_balance_params.ContractGetNetBalanceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ContractGetNetBalanceResponse,
+ )
+
def list_balances(
self,
*,
@@ -1262,6 +1376,8 @@ async def create(
net_payment_terms_days: float | Omit = omit,
netsuite_sales_order_id: str | Omit = omit,
overrides: Iterable[contract_create_params.Override] | Omit = omit,
+ package_alias: str | Omit = omit,
+ package_id: str | Omit = omit,
prepaid_balance_threshold_configuration: PrepaidBalanceThresholdConfiguration | Omit = omit,
priority: float | Omit = omit,
professional_services: Iterable[contract_create_params.ProfessionalService] | Omit = omit,
@@ -1432,6 +1548,13 @@ async def create(
netsuite_sales_order_id: This field's availability is dependent on your client's configuration.
+ package_alias: Selects the package linked to the specified alias as of the contract's start
+ date. Mutually exclusive with package_id.
+
+ package_id: If provided, provisions a customer on a package instead of creating a
+ traditional contract. When specified, only customer_id, starting_at, package_id,
+ and uniqueness_key are allowed.
+
priority: Priority of the contract.
professional_services: This field's availability is dependent on your client's configuration.
@@ -1488,6 +1611,8 @@ async def create(
"net_payment_terms_days": net_payment_terms_days,
"netsuite_sales_order_id": netsuite_sales_order_id,
"overrides": overrides,
+ "package_alias": package_alias,
+ "package_id": package_id,
"prepaid_balance_threshold_configuration": prepaid_balance_threshold_configuration,
"priority": priority,
"professional_services": professional_services,
@@ -1923,6 +2048,106 @@ async def create_historical_invoices(
cast_to=ContractCreateHistoricalInvoicesResponse,
)
+ async def get_net_balance(
+ self,
+ *,
+ customer_id: str,
+ credit_type_id: str | Omit = omit,
+ filters: Iterable[BalanceFilter] | Omit = omit,
+ invoice_inclusion_mode: Literal["FINALIZED", "FINALIZED_AND_DRAFT"] | Omit = omit,
+ # 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,
+ ) -> ContractGetNetBalanceResponse:
+ """
+ Retrieve the combined current balance across any grouping of credits and commits
+ for a customer in a single API call.
+
+ - Display real-time available balance to customers in billing dashboards
+ - Build finance dashboards showing credit utilization across customer segments
+ - Validate expected vs. actual balance during billing reconciliation
+
+ ### Key response fields:
+
+ - `balance`: The combined net balance available to use at this moment across all
+ matching commits and credits
+ - `credit_type_id`: The credit type (fiat or custom pricing unit) the balance is
+ denominated in
+
+ ### Filtering options:
+
+ Balance filters allow you to scope the calculation to specific subsets of
+ commits and credits. When using multiple filter objects, they are OR'd together
+ — if a commit or credit matches any filter, it's included in the net balance.
+ Within a single filter object, all specified conditions are AND'd together.
+
+ - **Balance types**: Include any combination of `PREPAID_COMMIT`,
+ `POSTPAID_COMMIT`, and `CREDIT` (e.g., `["PREPAID_COMMIT", "CREDIT"]` to
+ exclude postpaid commits). If not specified, all balance types are included.
+ - **Specific IDs**: Target exact commit or credit IDs for precise balance
+ queries
+ - **Custom fields**: Filter by custom field key-value pairs; when multiple pairs
+ are provided, commits must match all of them
+
+ **Example**: To get the balance of all free-trial credits OR all
+ signup-promotion commits, you'd pass two filter objects — one filtering for
+ CREDIT with custom field campaign: free-trial, and another filtering for
+ PREPAID_COMMIT with custom field campaign: signup-promotion.
+
+ ### Usage guidelines:
+
+ - **Draft invoice handling**: Use `invoice_inclusion_mode` to control whether
+ pending draft invoice deductions are included (`FINALIZED_AND_DRAFT`, the
+ default) or excluded (`FINALIZED`) from the balance calculation
+ - **Account hierarchies**: When querying a child customer, shared commits from
+ parent contracts are not included — query the parent customer directly to see
+ shared commit balances
+ - **Negative balances**: Manual ledger entries can cause negative segment
+ balances; these are treated as zero when calculating the net balance
+ - **Credit types**: If `credit_type_id` is not specified, the balance defaults
+ to USD (cents)
+
+ Args:
+ customer_id: The ID of the customer.
+
+ credit_type_id: The ID of the credit type (can be fiat or a custom pricing unit) to get the
+ balance for. Defaults to USD (cents) if not specified.
+
+ filters: Balance filters are OR'd together, so if a given commit or credit matches any of
+ the filters, it will be included in the net balance.
+
+ invoice_inclusion_mode: Controls which invoices are considered when calculating the remaining balance.
+ `FINALIZED` considers only deductions from finalized invoices.
+ `FINALIZED_AND_DRAFT` also includes deductions from pending draft invoices.
+
+ 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
+ """
+ return await self._post(
+ "/v1/contracts/customerBalances/getNetBalance",
+ body=await async_maybe_transform(
+ {
+ "customer_id": customer_id,
+ "credit_type_id": credit_type_id,
+ "filters": filters,
+ "invoice_inclusion_mode": invoice_inclusion_mode,
+ },
+ contract_get_net_balance_params.ContractGetNetBalanceParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ContractGetNetBalanceResponse,
+ )
+
def list_balances(
self,
*,
@@ -2372,6 +2597,9 @@ def __init__(self, contracts: ContractsResource) -> None:
self.create_historical_invoices = to_raw_response_wrapper(
contracts.create_historical_invoices,
)
+ self.get_net_balance = to_raw_response_wrapper(
+ contracts.get_net_balance,
+ )
self.list_balances = to_raw_response_wrapper(
contracts.list_balances,
)
@@ -2429,6 +2657,9 @@ def __init__(self, contracts: AsyncContractsResource) -> None:
self.create_historical_invoices = async_to_raw_response_wrapper(
contracts.create_historical_invoices,
)
+ self.get_net_balance = async_to_raw_response_wrapper(
+ contracts.get_net_balance,
+ )
self.list_balances = async_to_raw_response_wrapper(
contracts.list_balances,
)
@@ -2486,6 +2717,9 @@ def __init__(self, contracts: ContractsResource) -> None:
self.create_historical_invoices = to_streamed_response_wrapper(
contracts.create_historical_invoices,
)
+ self.get_net_balance = to_streamed_response_wrapper(
+ contracts.get_net_balance,
+ )
self.list_balances = to_streamed_response_wrapper(
contracts.list_balances,
)
@@ -2543,6 +2777,9 @@ def __init__(self, contracts: AsyncContractsResource) -> None:
self.create_historical_invoices = async_to_streamed_response_wrapper(
contracts.create_historical_invoices,
)
+ self.get_net_balance = async_to_streamed_response_wrapper(
+ contracts.get_net_balance,
+ )
self.list_balances = async_to_streamed_response_wrapper(
contracts.list_balances,
)
diff --git a/src/metronome/resources/v1/contracts/rate_cards/rates.py b/src/metronome/resources/v1/contracts/rate_cards/rates.py
index c50ad25b9..d11fba3ce 100644
--- a/src/metronome/resources/v1/contracts/rate_cards/rates.py
+++ b/src/metronome/resources/v1/contracts/rate_cards/rates.py
@@ -130,7 +130,7 @@ def add(
entitled: bool,
product_id: str,
rate_card_id: str,
- rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"],
+ rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"],
starting_at: Union[str, datetime],
billing_frequency: Literal["MONTHLY", "QUARTERLY", "ANNUAL", "WEEKLY"] | Omit = omit,
commit_rate: CommitRate | Omit = omit,
@@ -364,7 +364,7 @@ async def add(
entitled: bool,
product_id: str,
rate_card_id: str,
- rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"],
+ rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"],
starting_at: Union[str, datetime],
billing_frequency: Literal["MONTHLY", "QUARTERLY", "ANNUAL", "WEEKLY"] | Omit = omit,
commit_rate: CommitRate | Omit = omit,
diff --git a/src/metronome/resources/v2/contracts.py b/src/metronome/resources/v2/contracts.py
index ba7e55056..2c1630c08 100644
--- a/src/metronome/resources/v2/contracts.py
+++ b/src/metronome/resources/v2/contracts.py
@@ -222,6 +222,7 @@ def edit(
add_recurring_commits: Iterable[contract_edit_params.AddRecurringCommit] | Omit = omit,
add_recurring_credits: Iterable[contract_edit_params.AddRecurringCredit] | Omit = omit,
add_reseller_royalties: Iterable[contract_edit_params.AddResellerRoyalty] | Omit = omit,
+ add_revenue_system_configuration_update: contract_edit_params.AddRevenueSystemConfigurationUpdate | Omit = omit,
add_scheduled_charges: Iterable[contract_edit_params.AddScheduledCharge] | Omit = omit,
add_spend_threshold_configuration: SpendThresholdConfigurationV2 | Omit = omit,
add_subscriptions: Iterable[contract_edit_params.AddSubscription] | Omit = omit,
@@ -235,6 +236,7 @@ def edit(
update_contract_end_date: Union[str, datetime, None] | Omit = omit,
update_contract_name: Optional[str] | Omit = omit,
update_credits: Iterable[contract_edit_params.UpdateCredit] | Omit = omit,
+ update_net_payment_terms_days: Optional[float] | Omit = omit,
update_prepaid_balance_threshold_configuration: contract_edit_params.UpdatePrepaidBalanceThresholdConfiguration
| Omit = omit,
update_recurring_commits: Iterable[contract_edit_params.UpdateRecurringCommit] | Omit = omit,
@@ -284,6 +286,10 @@ def edit(
add_professional_services: This field's availability is dependent on your client's configuration.
+ add_revenue_system_configuration_update: Update the revenue system configuration on the contract. Currently only supports
+ adding a revenue system configuration to a contract that does not already have
+ one.
+
add_subscriptions: Optional list of
[subscriptions](https://docs.metronome.com/manage-product-access/create-subscription/)
to add to the contract.
@@ -308,6 +314,9 @@ def edit(
update_contract_name: Value to update the contract name to. If not provided, the contract name will
remain unchanged.
+ update_net_payment_terms_days: Number of days after issuance of invoice after which the invoice is due (e.g.
+ Net 30).
+
update_recurring_commits: Edits to these recurring commits will only affect commits whose access schedules
has not started. Expired commits, and commits with an active access schedule
will remain unchanged.
@@ -342,6 +351,7 @@ def edit(
"add_recurring_commits": add_recurring_commits,
"add_recurring_credits": add_recurring_credits,
"add_reseller_royalties": add_reseller_royalties,
+ "add_revenue_system_configuration_update": add_revenue_system_configuration_update,
"add_scheduled_charges": add_scheduled_charges,
"add_spend_threshold_configuration": add_spend_threshold_configuration,
"add_subscriptions": add_subscriptions,
@@ -355,6 +365,7 @@ def edit(
"update_contract_end_date": update_contract_end_date,
"update_contract_name": update_contract_name,
"update_credits": update_credits,
+ "update_net_payment_terms_days": update_net_payment_terms_days,
"update_prepaid_balance_threshold_configuration": update_prepaid_balance_threshold_configuration,
"update_recurring_commits": update_recurring_commits,
"update_recurring_credits": update_recurring_credits,
@@ -823,6 +834,7 @@ async def edit(
add_recurring_commits: Iterable[contract_edit_params.AddRecurringCommit] | Omit = omit,
add_recurring_credits: Iterable[contract_edit_params.AddRecurringCredit] | Omit = omit,
add_reseller_royalties: Iterable[contract_edit_params.AddResellerRoyalty] | Omit = omit,
+ add_revenue_system_configuration_update: contract_edit_params.AddRevenueSystemConfigurationUpdate | Omit = omit,
add_scheduled_charges: Iterable[contract_edit_params.AddScheduledCharge] | Omit = omit,
add_spend_threshold_configuration: SpendThresholdConfigurationV2 | Omit = omit,
add_subscriptions: Iterable[contract_edit_params.AddSubscription] | Omit = omit,
@@ -836,6 +848,7 @@ async def edit(
update_contract_end_date: Union[str, datetime, None] | Omit = omit,
update_contract_name: Optional[str] | Omit = omit,
update_credits: Iterable[contract_edit_params.UpdateCredit] | Omit = omit,
+ update_net_payment_terms_days: Optional[float] | Omit = omit,
update_prepaid_balance_threshold_configuration: contract_edit_params.UpdatePrepaidBalanceThresholdConfiguration
| Omit = omit,
update_recurring_commits: Iterable[contract_edit_params.UpdateRecurringCommit] | Omit = omit,
@@ -885,6 +898,10 @@ async def edit(
add_professional_services: This field's availability is dependent on your client's configuration.
+ add_revenue_system_configuration_update: Update the revenue system configuration on the contract. Currently only supports
+ adding a revenue system configuration to a contract that does not already have
+ one.
+
add_subscriptions: Optional list of
[subscriptions](https://docs.metronome.com/manage-product-access/create-subscription/)
to add to the contract.
@@ -909,6 +926,9 @@ async def edit(
update_contract_name: Value to update the contract name to. If not provided, the contract name will
remain unchanged.
+ update_net_payment_terms_days: Number of days after issuance of invoice after which the invoice is due (e.g.
+ Net 30).
+
update_recurring_commits: Edits to these recurring commits will only affect commits whose access schedules
has not started. Expired commits, and commits with an active access schedule
will remain unchanged.
@@ -943,6 +963,7 @@ async def edit(
"add_recurring_commits": add_recurring_commits,
"add_recurring_credits": add_recurring_credits,
"add_reseller_royalties": add_reseller_royalties,
+ "add_revenue_system_configuration_update": add_revenue_system_configuration_update,
"add_scheduled_charges": add_scheduled_charges,
"add_spend_threshold_configuration": add_spend_threshold_configuration,
"add_subscriptions": add_subscriptions,
@@ -956,6 +977,7 @@ async def edit(
"update_contract_end_date": update_contract_end_date,
"update_contract_name": update_contract_name,
"update_credits": update_credits,
+ "update_net_payment_terms_days": update_net_payment_terms_days,
"update_prepaid_balance_threshold_configuration": update_prepaid_balance_threshold_configuration,
"update_recurring_commits": update_recurring_commits,
"update_recurring_credits": update_recurring_credits,
diff --git a/src/metronome/types/__init__.py b/src/metronome/types/__init__.py
index fb4b18874..4c91d3f67 100644
--- a/src/metronome/types/__init__.py
+++ b/src/metronome/types/__init__.py
@@ -16,6 +16,7 @@
ProService as ProService,
OverrideTier as OverrideTier,
Subscription as Subscription,
+ BalanceFilter as BalanceFilter,
OverwriteRate as OverwriteRate,
CreditTypeData as CreditTypeData,
PropertyFilter as PropertyFilter,
diff --git a/src/metronome/types/shared/__init__.py b/src/metronome/types/shared/__init__.py
index 1afb1cea4..4dc775596 100644
--- a/src/metronome/types/shared/__init__.py
+++ b/src/metronome/types/shared/__init__.py
@@ -13,6 +13,7 @@
from .pro_service import ProService as ProService
from .subscription import Subscription as Subscription
from .override_tier import OverrideTier as OverrideTier
+from .balance_filter import BalanceFilter as BalanceFilter
from .overwrite_rate import OverwriteRate as OverwriteRate
from .property_filter import PropertyFilter as PropertyFilter
from .commit_specifier import CommitSpecifier as CommitSpecifier
diff --git a/src/metronome/types/shared/balance_filter.py b/src/metronome/types/shared/balance_filter.py
new file mode 100644
index 000000000..092eb54e6
--- /dev/null
+++ b/src/metronome/types/shared/balance_filter.py
@@ -0,0 +1,19 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, List, Optional
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["BalanceFilter"]
+
+
+class BalanceFilter(BaseModel):
+ balance_types: Optional[List[Literal["PREPAID_COMMIT", "POSTPAID_COMMIT", "CREDIT"]]] = None
+ """The balance type to filter by."""
+
+ custom_fields: Optional[Dict[str, str]] = None
+ """Custom fields to compute balance across. Must match all custom fields"""
+
+ ids: Optional[List[str]] = None
+ """Specific IDs to compute balance across."""
diff --git a/src/metronome/types/shared/commit_rate.py b/src/metronome/types/shared/commit_rate.py
index dd5e1f5d6..6dcef3b57 100644
--- a/src/metronome/types/shared/commit_rate.py
+++ b/src/metronome/types/shared/commit_rate.py
@@ -15,7 +15,7 @@ class CommitRate(BaseModel):
You can choose to use this rate rather than list rate when consuming a credit or commit.
"""
- rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"]
+ rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"]
price: Optional[float] = None
"""Commit rate price. For FLAT rate_type, this must be >=0."""
diff --git a/src/metronome/types/shared/commit_specifier_input.py b/src/metronome/types/shared/commit_specifier_input.py
index e6af499d5..fa745a9c7 100644
--- a/src/metronome/types/shared/commit_specifier_input.py
+++ b/src/metronome/types/shared/commit_specifier_input.py
@@ -9,8 +9,16 @@
class CommitSpecifierInput(BaseModel):
presentation_group_values: Optional[Dict[str, str]] = None
+ """
+ If provided, the specifier will apply to product usage with these set of
+ presentation group values.
+ """
pricing_group_values: Optional[Dict[str, str]] = None
+ """
+ If provided, the specifier will apply to product usage with these set of pricing
+ group values.
+ """
product_id: Optional[str] = None
"""
diff --git a/src/metronome/types/shared/contract.py b/src/metronome/types/shared/contract.py
index f9ac585b2..5c642c3df 100644
--- a/src/metronome/types/shared/contract.py
+++ b/src/metronome/types/shared/contract.py
@@ -127,6 +127,9 @@ class Contract(BaseModel):
customer_billing_provider_configuration: Optional[CustomerBillingProviderConfiguration] = None
"""The billing provider configuration associated with a contract."""
+ package_id: Optional[str] = None
+ """ID of the package this contract was created from, if applicable."""
+
prepaid_balance_threshold_configuration: Optional[PrepaidBalanceThresholdConfiguration] = None
priority: Optional[float] = None
diff --git a/src/metronome/types/shared/contract_v2.py b/src/metronome/types/shared/contract_v2.py
index 109187cab..3894d1199 100644
--- a/src/metronome/types/shared/contract_v2.py
+++ b/src/metronome/types/shared/contract_v2.py
@@ -399,7 +399,7 @@ class OverrideOverrideSpecifier(BaseModel):
class OverrideOverwriteRate(BaseModel):
- rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"]
+ rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"]
credit_type: Optional[CreditTypeData] = None
@@ -734,8 +734,8 @@ class HierarchyConfigurationParentHierarchyConfigurationChild(BaseModel):
class HierarchyConfigurationParentHierarchyConfigurationParentBehavior(BaseModel):
invoice_consolidation_type: Optional[Literal["CONCATENATE", "NONE"]] = None
"""
- Account hierarchy M3 - Indicates the desired behavior of consolidated invoices
- generated by the parent in a customer hierarchy
+ Indicates the desired behavior of consolidated invoices generated by the parent
+ in a customer hierarchy
**CONCATENATE**: Statements on the invoices of child customers will be appended
to the consolidated invoice
@@ -765,15 +765,15 @@ class HierarchyConfigurationChildHierarchyConfigurationV2(BaseModel):
payer: Optional[Literal["SELF", "PARENT"]] = None
"""
- Account hierarchy M3 - Indicates which customer should pay for the child's
- invoice charges **SELF**: The child pays for its own invoice charges **PARENT**:
- The parent pays for the child's invoice charges
+ Indicates which customer should pay for the child's invoice charges **SELF**:
+ The child pays for its own invoice charges **PARENT**: The parent pays for the
+ child's invoice charges
"""
usage_statement_behavior: Optional[Literal["CONSOLIDATE", "SEPARATE"]] = None
"""
- Account hierarchy M3 - Indicates the behavior of the child's invoice statements
- on the parent's invoices.
+ Indicates the behavior of the child's invoice statements on the parent's
+ invoices.
**CONSOLIDATE**: Child's invoice statements will be added to parent's
consolidated invoices
diff --git a/src/metronome/types/shared/hierarchy_configuration.py b/src/metronome/types/shared/hierarchy_configuration.py
index c9758dcf8..f7df3642d 100644
--- a/src/metronome/types/shared/hierarchy_configuration.py
+++ b/src/metronome/types/shared/hierarchy_configuration.py
@@ -24,8 +24,8 @@ class ParentHierarchyConfigurationChild(BaseModel):
class ParentHierarchyConfigurationParentBehavior(BaseModel):
invoice_consolidation_type: Optional[Literal["CONCATENATE", "NONE"]] = None
"""
- Account hierarchy M3 - Indicates the desired behavior of consolidated invoices
- generated by the parent in a customer hierarchy
+ Indicates the desired behavior of consolidated invoices generated by the parent
+ in a customer hierarchy
**CONCATENATE**: Statements on the invoices of child customers will be appended
to the consolidated invoice
@@ -54,9 +54,7 @@ class ChildHierarchyConfiguration(BaseModel):
"""The single parent contract/customer for this child."""
payer: Optional[Literal["SELF", "PARENT"]] = None
- """
- Account hierarchy M3 - Indicates which customer should pay for the child's
- invoice charges
+ """Indicates which customer should pay for the child's invoice charges
**SELF**: The child pays for its own invoice charges
@@ -65,8 +63,8 @@ class ChildHierarchyConfiguration(BaseModel):
usage_statement_behavior: Optional[Literal["CONSOLIDATE", "SEPARATE"]] = None
"""
- Account hierarchy M3 - Indicates the behavior of the child's invoice statements
- on the parent's invoices.
+ Indicates the behavior of the child's invoice statements on the parent's
+ invoices.
**CONSOLIDATE**: Child's invoice statements will be added to parent's
consolidated invoices
diff --git a/src/metronome/types/shared/override.py b/src/metronome/types/shared/override.py
index 2f3f077f3..e5ea19908 100644
--- a/src/metronome/types/shared/override.py
+++ b/src/metronome/types/shared/override.py
@@ -80,7 +80,7 @@ class Override(BaseModel):
quantity: Optional[float] = None
"""Default quantity. For SUBSCRIPTION rate_type, this must be >=0."""
- rate_type: Optional[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"]] = None
+ rate_type: Optional[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"]] = None
target: Optional[Literal["COMMIT_RATE", "LIST_RATE"]] = None
diff --git a/src/metronome/types/shared/overwrite_rate.py b/src/metronome/types/shared/overwrite_rate.py
index 1ba6850a9..19daf8f70 100644
--- a/src/metronome/types/shared/overwrite_rate.py
+++ b/src/metronome/types/shared/overwrite_rate.py
@@ -11,7 +11,7 @@
class OverwriteRate(BaseModel):
- rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"]
+ rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"]
credit_type: Optional[CreditTypeData] = None
diff --git a/src/metronome/types/shared/rate.py b/src/metronome/types/shared/rate.py
index a3e59f479..54e2d869d 100644
--- a/src/metronome/types/shared/rate.py
+++ b/src/metronome/types/shared/rate.py
@@ -11,7 +11,7 @@
class Rate(BaseModel):
- rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "CUSTOM", "TIERED"]
+ rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "CUSTOM", "TIERED", "TIERED_PERCENTAGE"]
credit_type: Optional[CreditTypeData] = None
diff --git a/src/metronome/types/shared_params/__init__.py b/src/metronome/types/shared_params/__init__.py
index 42e69a2fd..8fad226fd 100644
--- a/src/metronome/types/shared_params/__init__.py
+++ b/src/metronome/types/shared_params/__init__.py
@@ -2,6 +2,7 @@
from .tier import Tier as Tier
from .commit_rate import CommitRate as CommitRate
+from .balance_filter import BalanceFilter as BalanceFilter
from .property_filter import PropertyFilter as PropertyFilter
from .base_usage_filter import BaseUsageFilter as BaseUsageFilter
from .event_type_filter import EventTypeFilter as EventTypeFilter
diff --git a/src/metronome/types/shared_params/balance_filter.py b/src/metronome/types/shared_params/balance_filter.py
new file mode 100644
index 000000000..edc38bb4c
--- /dev/null
+++ b/src/metronome/types/shared_params/balance_filter.py
@@ -0,0 +1,21 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, List
+from typing_extensions import Literal, TypedDict
+
+from ..._types import SequenceNotStr
+
+__all__ = ["BalanceFilter"]
+
+
+class BalanceFilter(TypedDict, total=False):
+ balance_types: List[Literal["PREPAID_COMMIT", "POSTPAID_COMMIT", "CREDIT"]]
+ """The balance type to filter by."""
+
+ custom_fields: Dict[str, str]
+ """Custom fields to compute balance across. Must match all custom fields"""
+
+ ids: SequenceNotStr[str]
+ """Specific IDs to compute balance across."""
diff --git a/src/metronome/types/shared_params/commit_rate.py b/src/metronome/types/shared_params/commit_rate.py
index e49852fab..5e70a4502 100644
--- a/src/metronome/types/shared_params/commit_rate.py
+++ b/src/metronome/types/shared_params/commit_rate.py
@@ -16,7 +16,7 @@ class CommitRate(TypedDict, total=False):
You can choose to use this rate rather than list rate when consuming a credit or commit.
"""
- rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"]]
+ rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"]]
price: float
"""Commit rate price. For FLAT rate_type, this must be >=0."""
diff --git a/src/metronome/types/shared_params/commit_specifier_input.py b/src/metronome/types/shared_params/commit_specifier_input.py
index b51968f86..3c1263c4b 100644
--- a/src/metronome/types/shared_params/commit_specifier_input.py
+++ b/src/metronome/types/shared_params/commit_specifier_input.py
@@ -12,8 +12,16 @@
class CommitSpecifierInput(TypedDict, total=False):
presentation_group_values: Dict[str, str]
+ """
+ If provided, the specifier will apply to product usage with these set of
+ presentation group values.
+ """
pricing_group_values: Dict[str, str]
+ """
+ If provided, the specifier will apply to product usage with these set of pricing
+ group values.
+ """
product_id: str
"""
diff --git a/src/metronome/types/v1/__init__.py b/src/metronome/types/v1/__init__.py
index a6c61abec..cc59172ee 100644
--- a/src/metronome/types/v1/__init__.py
+++ b/src/metronome/types/v1/__init__.py
@@ -79,6 +79,7 @@
from .custom_field_set_values_params import CustomFieldSetValuesParams as CustomFieldSetValuesParams
from .customer_preview_events_params import CustomerPreviewEventsParams as CustomerPreviewEventsParams
from .billable_metric_create_response import BillableMetricCreateResponse as BillableMetricCreateResponse
+from .contract_get_net_balance_params import ContractGetNetBalanceParams as ContractGetNetBalanceParams
from .contract_list_balances_response import ContractListBalancesResponse as ContractListBalancesResponse
from .contract_update_end_date_params import ContractUpdateEndDateParams as ContractUpdateEndDateParams
from .custom_field_list_keys_response import CustomFieldListKeysResponse as CustomFieldListKeysResponse
@@ -89,6 +90,7 @@
from .customer_preview_events_response import CustomerPreviewEventsResponse as CustomerPreviewEventsResponse
from .rollover_amount_max_amount_param import RolloverAmountMaxAmountParam as RolloverAmountMaxAmountParam
from .billable_metric_retrieve_response import BillableMetricRetrieveResponse as BillableMetricRetrieveResponse
+from .contract_get_net_balance_response import ContractGetNetBalanceResponse as ContractGetNetBalanceResponse
from .contract_update_end_date_response import ContractUpdateEndDateResponse as ContractUpdateEndDateResponse
from .custom_field_delete_values_params import CustomFieldDeleteValuesParams as CustomFieldDeleteValuesParams
from .credit_grant_list_entries_response import CreditGrantListEntriesResponse as CreditGrantListEntriesResponse
diff --git a/src/metronome/types/v1/contract_amend_params.py b/src/metronome/types/v1/contract_amend_params.py
index 712992637..d80404a04 100644
--- a/src/metronome/types/v1/contract_amend_params.py
+++ b/src/metronome/types/v1/contract_amend_params.py
@@ -577,7 +577,7 @@ class OverrideOverrideSpecifier(TypedDict, total=False):
class OverrideOverwriteRate(TypedDict, total=False):
"""Required for OVERWRITE type."""
- rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"]]
+ rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"]]
credit_type_id: str
diff --git a/src/metronome/types/v1/contract_create_params.py b/src/metronome/types/v1/contract_create_params.py
index 833935d61..e0e152f90 100644
--- a/src/metronome/types/v1/contract_create_params.py
+++ b/src/metronome/types/v1/contract_create_params.py
@@ -115,6 +115,19 @@ class ContractCreateParams(TypedDict, total=False):
overrides: Iterable[Override]
+ package_alias: str
+ """
+ Selects the package linked to the specified alias as of the contract's start
+ date. Mutually exclusive with package_id.
+ """
+
+ package_id: str
+ """
+ If provided, provisions a customer on a package instead of creating a
+ traditional contract. When specified, only customer_id, starting_at, package_id,
+ and uniqueness_key are allowed.
+ """
+
prepaid_balance_threshold_configuration: PrepaidBalanceThresholdConfiguration
priority: float
@@ -649,8 +662,8 @@ class HierarchyConfigurationParent(TypedDict, total=False):
class HierarchyConfigurationParentBehavior(TypedDict, total=False):
invoice_consolidation_type: Literal["CONCATENATE", "NONE"]
"""
- Account hierarchy M3 - Indicates the desired behavior of consolidated invoices
- generated by the parent in a customer hierarchy
+ Indicates the desired behavior of consolidated invoices generated by the parent
+ in a customer hierarchy
**CONCATENATE**: Statements on the invoices of child customers will be appended
to the consolidated invoice
@@ -665,9 +678,7 @@ class HierarchyConfiguration(TypedDict, total=False):
parent_behavior: HierarchyConfigurationParentBehavior
payer: Literal["SELF", "PARENT"]
- """
- Account hierarchy M3 - Indicates which customer should pay for the child's
- invoice charges
+ """Indicates which customer should pay for the child's invoice charges
**SELF**: The child pays for its own invoice charges
@@ -676,8 +687,8 @@ class HierarchyConfiguration(TypedDict, total=False):
usage_statement_behavior: Literal["CONSOLIDATE", "SEPARATE"]
"""
- Account hierarchy M3 - Indicates the behavior of the child's invoice statements
- on the parent's invoices.
+ Indicates the behavior of the child's invoice statements on the parent's
+ invoices.
**CONSOLIDATE**: Child's invoice statements will be added to parent's
consolidated invoices
@@ -744,7 +755,7 @@ class OverrideOverrideSpecifier(TypedDict, total=False):
class OverrideOverwriteRate(TypedDict, total=False):
"""Required for OVERWRITE type."""
- rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"]]
+ rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"]]
credit_type_id: str
diff --git a/src/metronome/types/v1/contract_get_net_balance_params.py b/src/metronome/types/v1/contract_get_net_balance_params.py
new file mode 100644
index 000000000..a0e19599a
--- /dev/null
+++ b/src/metronome/types/v1/contract_get_net_balance_params.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Iterable
+from typing_extensions import Literal, Required, TypedDict
+
+from ..shared_params.balance_filter import BalanceFilter
+
+__all__ = ["ContractGetNetBalanceParams"]
+
+
+class ContractGetNetBalanceParams(TypedDict, total=False):
+ customer_id: Required[str]
+ """The ID of the customer."""
+
+ credit_type_id: str
+ """
+ The ID of the credit type (can be fiat or a custom pricing unit) to get the
+ balance for. Defaults to USD (cents) if not specified.
+ """
+
+ filters: Iterable[BalanceFilter]
+ """
+ Balance filters are OR'd together, so if a given commit or credit matches any of
+ the filters, it will be included in the net balance.
+ """
+
+ invoice_inclusion_mode: Literal["FINALIZED", "FINALIZED_AND_DRAFT"]
+ """Controls which invoices are considered when calculating the remaining balance.
+
+ `FINALIZED` considers only deductions from finalized invoices.
+ `FINALIZED_AND_DRAFT` also includes deductions from pending draft invoices.
+ """
diff --git a/src/metronome/types/v1/contract_get_net_balance_response.py b/src/metronome/types/v1/contract_get_net_balance_response.py
new file mode 100644
index 000000000..15771b4b7
--- /dev/null
+++ b/src/metronome/types/v1/contract_get_net_balance_response.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from ..._models import BaseModel
+
+__all__ = ["ContractGetNetBalanceResponse", "Data"]
+
+
+class Data(BaseModel):
+ balance: float
+ """
+ The combined net balance that the customer has access to use at this moment
+ across all pertinent commits and credits.
+ """
+
+ credit_type_id: str
+ """
+ The ID of the credit type (can be fiat or a custom pricing unit) that the
+ balance is for.
+ """
+
+
+class ContractGetNetBalanceResponse(BaseModel):
+ data: Data
diff --git a/src/metronome/types/v1/contracts/rate_cards/rate_add_many_params.py b/src/metronome/types/v1/contracts/rate_cards/rate_add_many_params.py
index 17c4b3fd8..331d3a2ed 100644
--- a/src/metronome/types/v1/contracts/rate_cards/rate_add_many_params.py
+++ b/src/metronome/types/v1/contracts/rate_cards/rate_add_many_params.py
@@ -25,7 +25,7 @@ class Rate(TypedDict, total=False):
product_id: Required[str]
"""ID of the product to add a rate for"""
- rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"]]
+ rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"]]
starting_at: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]]
"""inclusive effective date"""
diff --git a/src/metronome/types/v1/contracts/rate_cards/rate_add_params.py b/src/metronome/types/v1/contracts/rate_cards/rate_add_params.py
index 1e5b58a64..16041b15b 100644
--- a/src/metronome/types/v1/contracts/rate_cards/rate_add_params.py
+++ b/src/metronome/types/v1/contracts/rate_cards/rate_add_params.py
@@ -22,7 +22,7 @@ class RateAddParams(TypedDict, total=False):
rate_card_id: Required[str]
"""ID of the rate card to update"""
- rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"]]
+ rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"]]
starting_at: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]]
"""inclusive effective date"""
diff --git a/src/metronome/types/v1/contracts/rate_cards/rate_add_response.py b/src/metronome/types/v1/contracts/rate_cards/rate_add_response.py
index e61c837dd..c3cdde912 100644
--- a/src/metronome/types/v1/contracts/rate_cards/rate_add_response.py
+++ b/src/metronome/types/v1/contracts/rate_cards/rate_add_response.py
@@ -12,7 +12,7 @@
class Data(BaseModel):
- rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "CUSTOM", "TIERED"]
+ rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "CUSTOM", "TIERED", "TIERED_PERCENTAGE"]
commit_rate: Optional[CommitRate] = None
"""A distinct rate on the rate card.
diff --git a/src/metronome/types/v1/customers/invoice.py b/src/metronome/types/v1/customers/invoice.py
index 969c3f802..df735f1c7 100644
--- a/src/metronome/types/v1/customers/invoice.py
+++ b/src/metronome/types/v1/customers/invoice.py
@@ -45,8 +45,9 @@ class LineItemAppliedCommitOrCredit(BaseModel):
class LineItemOrigin(BaseModel):
- """
- Account hierarchy M3 - Present on line items from invoices with type USAGE_CONSOLIDATED. Indicates the original customer, contract, invoice and line item from which this line item was copied.
+ """Present on line items from invoices with type USAGE_CONSOLIDATED.
+
+ Indicates the original customer, contract, invoice and line item from which this line item was copied.
"""
contract_id: str
@@ -225,10 +226,10 @@ class LineItem(BaseModel):
netsuite_item_id: Optional[str] = None
origin: Optional[LineItemOrigin] = None
- """
- Account hierarchy M3 - Present on line items from invoices with type
- USAGE_CONSOLIDATED. Indicates the original customer, contract, invoice and line
- item from which this line item was copied.
+ """Present on line items from invoices with type USAGE_CONSOLIDATED.
+
+ Indicates the original customer, contract, invoice and line item from which this
+ line item was copied.
"""
postpaid_commit: Optional[LineItemPostpaidCommit] = None
@@ -460,7 +461,7 @@ class InvoiceAdjustment(BaseModel):
class Payer(BaseModel):
- """Account hierarchy M3 - Required for account hierarchy usage invoices.
+ """Required for account hierarchy usage invoices.
An object containing the contract and customer UUIDs that pay for this invoice.
"""
@@ -532,7 +533,7 @@ class Invoice(BaseModel):
"""This field's availability is dependent on your client's configuration."""
constituent_invoices: Optional[List[ConstituentInvoice]] = None
- """Account hierarchy M3 - Required on invoices with type USAGE_CONSOLIDATED.
+ """Required on invoices with type USAGE_CONSOLIDATED.
List of constituent invoices that were consolidated to create this invoice.
"""
@@ -571,7 +572,7 @@ class Invoice(BaseModel):
"""This field's availability is dependent on your client's configuration."""
payer: Optional[Payer] = None
- """Account hierarchy M3 - Required for account hierarchy usage invoices.
+ """Required for account hierarchy usage invoices.
An object containing the contract and customer UUIDs that pay for this invoice.
"""
diff --git a/src/metronome/types/v2/contract_edit_params.py b/src/metronome/types/v2/contract_edit_params.py
index caef9ae61..3f0d824e2 100644
--- a/src/metronome/types/v2/contract_edit_params.py
+++ b/src/metronome/types/v2/contract_edit_params.py
@@ -56,6 +56,9 @@
"AddResellerRoyalty",
"AddResellerRoyaltyAwsOptions",
"AddResellerRoyaltyGcpOptions",
+ "AddRevenueSystemConfigurationUpdate",
+ "AddRevenueSystemConfigurationUpdateRevenueSystemConfiguration",
+ "AddRevenueSystemConfigurationUpdateSchedule",
"AddScheduledCharge",
"AddScheduledChargeSchedule",
"AddScheduledChargeScheduleRecurringSchedule",
@@ -140,6 +143,13 @@ class ContractEditParams(TypedDict, total=False):
add_reseller_royalties: Iterable[AddResellerRoyalty]
+ add_revenue_system_configuration_update: AddRevenueSystemConfigurationUpdate
+ """Update the revenue system configuration on the contract.
+
+ Currently only supports adding a revenue system configuration to a contract that
+ does not already have one.
+ """
+
add_scheduled_charges: Iterable[AddScheduledCharge]
add_spend_threshold_configuration: SpendThresholdConfigurationV2
@@ -187,6 +197,12 @@ class ContractEditParams(TypedDict, total=False):
update_credits: Iterable[UpdateCredit]
+ update_net_payment_terms_days: Optional[float]
+ """Number of days after issuance of invoice after which the invoice is due (e.g.
+
+ Net 30).
+ """
+
update_prepaid_balance_threshold_configuration: UpdatePrepaidBalanceThresholdConfiguration
update_recurring_commits: Iterable[UpdateRecurringCommit]
@@ -745,7 +761,7 @@ class AddOverrideOverrideSpecifier(TypedDict, total=False):
class AddOverrideOverwriteRate(TypedDict, total=False):
"""Required for OVERWRITE type."""
- rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"]]
+ rate_type: Required[Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"]]
credit_type_id: str
@@ -1181,6 +1197,31 @@ class AddResellerRoyalty(TypedDict, total=False):
starting_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]
+class AddRevenueSystemConfigurationUpdateRevenueSystemConfiguration(TypedDict, total=False):
+ delivery_method: Literal["direct_to_billing_provider", "aws_sqs", "tackle", "aws_sns"]
+
+ provider: Literal["netsuite"]
+ """The revenue system provider type."""
+
+ revenue_system_configuration_id: str
+
+
+class AddRevenueSystemConfigurationUpdateSchedule(TypedDict, total=False):
+ effective_at: Required[Literal["START_OF_CURRENT_PERIOD"]]
+ """When the revenue system configuration update will take effect."""
+
+
+class AddRevenueSystemConfigurationUpdate(TypedDict, total=False):
+ """Update the revenue system configuration on the contract.
+
+ Currently only supports adding a revenue system configuration to a contract that does not already have one.
+ """
+
+ revenue_system_configuration: Required[AddRevenueSystemConfigurationUpdateRevenueSystemConfiguration]
+
+ schedule: Required[AddRevenueSystemConfigurationUpdateSchedule]
+
+
class AddScheduledChargeScheduleRecurringSchedule(TypedDict, total=False):
"""Enter the unit price and quantity for the charge or instead only send the amount.
diff --git a/src/metronome/types/v2/contract_get_edit_history_response.py b/src/metronome/types/v2/contract_get_edit_history_response.py
index d8a341e89..90ee55ee6 100644
--- a/src/metronome/types/v2/contract_get_edit_history_response.py
+++ b/src/metronome/types/v2/contract_get_edit_history_response.py
@@ -250,7 +250,7 @@ class DataAddOverrideOverrideSpecifier(BaseModel):
class DataAddOverrideOverwriteRate(BaseModel):
- rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "CUSTOM"]
+ rate_type: Literal["FLAT", "PERCENTAGE", "SUBSCRIPTION", "TIERED", "TIERED_PERCENTAGE", "CUSTOM"]
credit_type: Optional[CreditTypeData] = None
diff --git a/tests/api_resources/v1/test_contracts.py b/tests/api_resources/v1/test_contracts.py
index f3bd3ac6b..9313bd09b 100644
--- a/tests/api_resources/v1/test_contracts.py
+++ b/tests/api_resources/v1/test_contracts.py
@@ -17,6 +17,7 @@
ContractArchiveResponse,
ContractRetrieveResponse,
ContractListBalancesResponse,
+ ContractGetNetBalanceResponse,
ContractUpdateEndDateResponse,
ContractRetrieveRateScheduleResponse,
ContractCreateHistoricalInvoicesResponse,
@@ -241,6 +242,8 @@ def test_method_create_with_all_params(self, client: Metronome) -> None:
"type": "OVERWRITE",
}
],
+ package_alias="package_alias",
+ package_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
prepaid_balance_threshold_configuration={
"commit": {
"product_id": "product_id",
@@ -1077,6 +1080,58 @@ def test_streaming_response_create_historical_invoices(self, client: Metronome)
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_method_get_net_balance(self, client: Metronome) -> None:
+ contract = client.v1.contracts.get_net_balance(
+ customer_id="13117714-3f05-48e5-a6e9-a66093f13b4d",
+ )
+ assert_matches_type(ContractGetNetBalanceResponse, contract, path=["response"])
+
+ @parametrize
+ def test_method_get_net_balance_with_all_params(self, client: Metronome) -> None:
+ contract = client.v1.contracts.get_net_balance(
+ customer_id="13117714-3f05-48e5-a6e9-a66093f13b4d",
+ credit_type_id="2714e483-4ff1-48e4-9e25-ac732e8f24f2",
+ filters=[
+ {
+ "balance_types": ["CREDIT"],
+ "custom_fields": {"campaign": "free-trial"},
+ "ids": ["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"],
+ },
+ {
+ "balance_types": ["PREPAID_COMMIT", "POSTPAID_COMMIT"],
+ "custom_fields": {"campaign": "signup-promotion"},
+ "ids": ["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"],
+ },
+ ],
+ invoice_inclusion_mode="FINALIZED",
+ )
+ assert_matches_type(ContractGetNetBalanceResponse, contract, path=["response"])
+
+ @parametrize
+ def test_raw_response_get_net_balance(self, client: Metronome) -> None:
+ response = client.v1.contracts.with_raw_response.get_net_balance(
+ customer_id="13117714-3f05-48e5-a6e9-a66093f13b4d",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ contract = response.parse()
+ assert_matches_type(ContractGetNetBalanceResponse, contract, path=["response"])
+
+ @parametrize
+ def test_streaming_response_get_net_balance(self, client: Metronome) -> None:
+ with client.v1.contracts.with_streaming_response.get_net_balance(
+ customer_id="13117714-3f05-48e5-a6e9-a66093f13b4d",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ contract = response.parse()
+ assert_matches_type(ContractGetNetBalanceResponse, contract, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
def test_method_list_balances(self, client: Metronome) -> None:
contract = client.v1.contracts.list_balances(
@@ -1586,6 +1641,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncMetronome)
"type": "OVERWRITE",
}
],
+ package_alias="package_alias",
+ package_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
prepaid_balance_threshold_configuration={
"commit": {
"product_id": "product_id",
@@ -2422,6 +2479,58 @@ async def test_streaming_response_create_historical_invoices(self, async_client:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_method_get_net_balance(self, async_client: AsyncMetronome) -> None:
+ contract = await async_client.v1.contracts.get_net_balance(
+ customer_id="13117714-3f05-48e5-a6e9-a66093f13b4d",
+ )
+ assert_matches_type(ContractGetNetBalanceResponse, contract, path=["response"])
+
+ @parametrize
+ async def test_method_get_net_balance_with_all_params(self, async_client: AsyncMetronome) -> None:
+ contract = await async_client.v1.contracts.get_net_balance(
+ customer_id="13117714-3f05-48e5-a6e9-a66093f13b4d",
+ credit_type_id="2714e483-4ff1-48e4-9e25-ac732e8f24f2",
+ filters=[
+ {
+ "balance_types": ["CREDIT"],
+ "custom_fields": {"campaign": "free-trial"},
+ "ids": ["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"],
+ },
+ {
+ "balance_types": ["PREPAID_COMMIT", "POSTPAID_COMMIT"],
+ "custom_fields": {"campaign": "signup-promotion"},
+ "ids": ["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"],
+ },
+ ],
+ invoice_inclusion_mode="FINALIZED",
+ )
+ assert_matches_type(ContractGetNetBalanceResponse, contract, path=["response"])
+
+ @parametrize
+ async def test_raw_response_get_net_balance(self, async_client: AsyncMetronome) -> None:
+ response = await async_client.v1.contracts.with_raw_response.get_net_balance(
+ customer_id="13117714-3f05-48e5-a6e9-a66093f13b4d",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ contract = await response.parse()
+ assert_matches_type(ContractGetNetBalanceResponse, contract, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_get_net_balance(self, async_client: AsyncMetronome) -> None:
+ async with async_client.v1.contracts.with_streaming_response.get_net_balance(
+ customer_id="13117714-3f05-48e5-a6e9-a66093f13b4d",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ contract = await response.parse()
+ assert_matches_type(ContractGetNetBalanceResponse, contract, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
async def test_method_list_balances(self, async_client: AsyncMetronome) -> None:
contract = await async_client.v1.contracts.list_balances(
diff --git a/tests/api_resources/v2/test_contracts.py b/tests/api_resources/v2/test_contracts.py
index c490a7d17..91c100984 100644
--- a/tests/api_resources/v2/test_contracts.py
+++ b/tests/api_resources/v2/test_contracts.py
@@ -463,6 +463,14 @@ def test_method_edit_with_all_params(self, client: Metronome) -> None:
"starting_at": parse_datetime("2019-12-27T18:11:19.117Z"),
}
],
+ add_revenue_system_configuration_update={
+ "revenue_system_configuration": {
+ "delivery_method": "direct_to_billing_provider",
+ "provider": "netsuite",
+ "revenue_system_configuration_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ },
+ "schedule": {"effective_at": "START_OF_CURRENT_PERIOD"},
+ },
add_scheduled_charges=[
{
"product_id": "2e30f074-d04c-412e-a134-851ebfa5ceb2",
@@ -632,6 +640,7 @@ def test_method_edit_with_all_params(self, client: Metronome) -> None:
"rate_type": "LIST_RATE",
}
],
+ update_net_payment_terms_days=0,
update_prepaid_balance_threshold_configuration={
"commit": {
"description": "description",
@@ -1461,6 +1470,14 @@ async def test_method_edit_with_all_params(self, async_client: AsyncMetronome) -
"starting_at": parse_datetime("2019-12-27T18:11:19.117Z"),
}
],
+ add_revenue_system_configuration_update={
+ "revenue_system_configuration": {
+ "delivery_method": "direct_to_billing_provider",
+ "provider": "netsuite",
+ "revenue_system_configuration_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ },
+ "schedule": {"effective_at": "START_OF_CURRENT_PERIOD"},
+ },
add_scheduled_charges=[
{
"product_id": "2e30f074-d04c-412e-a134-851ebfa5ceb2",
@@ -1630,6 +1647,7 @@ async def test_method_edit_with_all_params(self, async_client: AsyncMetronome) -
"rate_type": "LIST_RATE",
}
],
+ update_net_payment_terms_days=0,
update_prepaid_balance_threshold_configuration={
"commit": {
"description": "description",
diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py
new file mode 100644
index 000000000..785aed7fe
--- /dev/null
+++ b/tests/test_utils/test_json.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import datetime
+from typing import Union
+
+import pydantic
+
+from metronome import _compat
+from metronome._utils._json import openapi_dumps
+
+
+class TestOpenapiDumps:
+ def test_basic(self) -> None:
+ data = {"key": "value", "number": 42}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"key":"value","number":42}'
+
+ def test_datetime_serialization(self) -> None:
+ dt = datetime.datetime(2023, 1, 1, 12, 0, 0)
+ data = {"datetime": dt}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}'
+
+ def test_pydantic_model_serialization(self) -> None:
+ class User(pydantic.BaseModel):
+ first_name: str
+ last_name: str
+ age: int
+
+ model_instance = User(first_name="John", last_name="Kramer", age=83)
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}'
+
+ def test_pydantic_model_with_default_values(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ role: str = "user"
+ active: bool = True
+ score: int = 0
+
+ model_instance = User(name="Alice")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Alice"}}'
+
+ def test_pydantic_model_with_default_values_overridden(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ role: str = "user"
+ active: bool = True
+
+ model_instance = User(name="Bob", role="admin", active=False)
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}'
+
+ def test_pydantic_model_with_alias(self) -> None:
+ class User(pydantic.BaseModel):
+ first_name: str = pydantic.Field(alias="firstName")
+ last_name: str = pydantic.Field(alias="lastName")
+
+ model_instance = User(firstName="John", lastName="Doe")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}'
+
+ def test_pydantic_model_with_alias_and_default(self) -> None:
+ class User(pydantic.BaseModel):
+ user_name: str = pydantic.Field(alias="userName")
+ user_role: str = pydantic.Field(default="member", alias="userRole")
+ is_active: bool = pydantic.Field(default=True, alias="isActive")
+
+ model_instance = User(userName="charlie")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"userName":"charlie"}}'
+
+ model_with_overrides = User(userName="diana", userRole="admin", isActive=False)
+ data = {"model": model_with_overrides}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}'
+
+ def test_pydantic_model_with_nested_models_and_defaults(self) -> None:
+ class Address(pydantic.BaseModel):
+ street: str
+ city: str = "Unknown"
+
+ class User(pydantic.BaseModel):
+ name: str
+ address: Address
+ verified: bool = False
+
+ if _compat.PYDANTIC_V1:
+ # to handle forward references in Pydantic v1
+ User.update_forward_refs(**locals()) # type: ignore[reportDeprecated]
+
+ address = Address(street="123 Main St")
+ user = User(name="Diana", address=address)
+ data = {"user": user}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}'
+
+ address_with_city = Address(street="456 Oak Ave", city="Boston")
+ user_verified = User(name="Eve", address=address_with_city, verified=True)
+ data = {"user": user_verified}
+ json_bytes = openapi_dumps(data)
+ assert (
+ json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}'
+ )
+
+ def test_pydantic_model_with_optional_fields(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ email: Union[str, None]
+ phone: Union[str, None]
+
+ model_with_none = User(name="Eve", email=None, phone=None)
+ data = {"model": model_with_none}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}'
+
+ model_with_values = User(name="Frank", email="frank@example.com", phone=None)
+ data = {"model": model_with_values}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}'