From 611cb752d4d71443a5f1c6c22d24394a87ffa295 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:14:20 +0000 Subject: [PATCH 1/3] feat(vehicles): used-vehicle CRUD + bulk insert for v1.1.0 --- .stats.yml | 4 +- README.md | 19 +- src/partnermax/resources/dealers/dealers.py | 182 ++++++----------- src/partnermax/resources/dealers/nlt/nlt.py | 18 -- .../resources/dealers/nlt/offers.py | 108 +++++----- .../resources/dealers/nlt_settings.py | 116 ++++++----- src/partnermax/resources/keys.py | 60 +++--- src/partnermax/types/dealer_create_params.py | 18 +- src/partnermax/types/dealer_detail.py | 63 +++--- src/partnermax/types/dealer_list_params.py | 6 +- src/partnermax/types/dealer_list_response.py | 3 +- src/partnermax/types/dealer_summary.py | 2 + src/partnermax/types/dealer_update_params.py | 22 +-- .../types/dealers/down_payment_tiers.py | 103 +++++++--- .../types/dealers/down_payment_tiers_param.py | 103 +++++++--- .../types/dealers/nlt/nlt_offer_summary.py | 43 ++-- .../types/dealers/nlt/offer_list_params.py | 29 +-- .../types/dealers/nlt/offer_list_response.py | 2 + .../dealers/nlt/offer_retrieve_response.py | 187 ++++++++++-------- .../dealers/nlt_setting_update_params.py | 18 +- src/partnermax/types/dealers/nlt_settings.py | 32 ++- src/partnermax/types/key_issue_params.py | 2 +- src/partnermax/types/key_issue_response.py | 15 +- src/partnermax/types/key_list_response.py | 8 +- .../api_resources/dealers/nlt/test_offers.py | 8 +- .../dealers/test_nlt_settings.py | 74 +++---- tests/api_resources/test_dealers.py | 146 ++++++++------ tests/api_resources/test_keys.py | 20 +- 28 files changed, 725 insertions(+), 686 deletions(-) diff --git a/.stats.yml b/.stats.yml index c73ca81..a686673 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 12 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-38113ab2a8a24debc22a07ed35d7f367b7faf132cdf0db5228c1d4c68d7f41f5.yml -openapi_spec_hash: 420922592a8c90a9fe4b811b9864ddc1 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-1f6135961fb19ff430c509fdffab0b2441ce875d3da69971921e4d231f57b25c.yml +openapi_spec_hash: c97c1474daf815592ca1a3a9fd29e482 config_hash: bf921bb2f4db038ffed15141c74cd018 diff --git a/README.md b/README.md index 0bb54d9..13465f4 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,17 @@ client = Partnermax( ) dealer_detail = client.dealers.create( + address="xx", business_name="Rossi Automobili S.R.L.", + city="xx", contact_email="info@rossi-auto.it", + contact_phone="xxxxx", postal_code="20121", primary_domain="rossi-auto.it", province_code="MI", vat_number="IT01234567890", ) +print(dealer_detail.dealer_id) ``` While you can provide an `api_key` keyword argument, @@ -67,13 +71,17 @@ client = AsyncPartnermax( async def main() -> None: dealer_detail = await client.dealers.create( + address="xx", business_name="Rossi Automobili S.R.L.", + city="xx", contact_email="info@rossi-auto.it", + contact_phone="xxxxx", postal_code="20121", primary_domain="rossi-auto.it", province_code="MI", vat_number="IT01234567890", ) + print(dealer_detail.dealer_id) asyncio.run(main()) @@ -107,13 +115,17 @@ async def main() -> None: http_client=DefaultAioHttpClient(), ) as client: dealer_detail = await client.dealers.create( + address="xx", business_name="Rossi Automobili S.R.L.", + city="xx", contact_email="info@rossi-auto.it", + contact_phone="xxxxx", postal_code="20121", primary_domain="rossi-auto.it", province_code="MI", vat_number="IT01234567890", ) + print(dealer_detail.dealer_id) asyncio.run(main()) @@ -139,11 +151,11 @@ client = Partnermax() nlt_settings = client.dealers.nlt_settings.update( dealer_id="dealer_id", - agency_markup_percent=3.5, + agency_markup_percent=0, down_payment_tiers={ "high": { "fixed_eur": 0, - "percent_of_list": 25, + "percent_of_list": 0, }, "low": { "fixed_eur": 0, @@ -151,10 +163,9 @@ nlt_settings = client.dealers.nlt_settings.update( }, "medium": { "fixed_eur": 0, - "percent_of_list": 12.5, + "percent_of_list": 0, }, }, - image_mode="branded", ) print(nlt_settings.down_payment_tiers) ``` diff --git a/src/partnermax/resources/dealers/dealers.py b/src/partnermax/resources/dealers/dealers.py index 6b38d94..1dadb84 100644 --- a/src/partnermax/resources/dealers/dealers.py +++ b/src/partnermax/resources/dealers/dealers.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict +from typing import Dict, Optional from typing_extensions import Literal import httpx @@ -46,7 +46,6 @@ class DealersResource(SyncAPIResource): @cached_property def nlt_settings(self) -> NltSettingsResource: - """Per-dealer NLT economics: agency markup percent and three down-payment tiers.""" return NltSettingsResource(self._client) @cached_property @@ -75,14 +74,16 @@ def with_streaming_response(self) -> DealersResourceWithStreamingResponse: def create( self, *, + address: str, business_name: str, + city: str, contact_email: str, + contact_phone: str, postal_code: str, primary_domain: str, province_code: str, vat_number: str, activate: bool | Omit = omit, - contact_phone: str | Omit = omit, metadata: Dict[str, str] | Omit = omit, idempotency_key: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -92,32 +93,10 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DealerDetail: - """Creates a new dealer as a child of the calling partner account. - - The dealer is - indexed in the cross-network AI-citation surfaces (MCP, Custom GPT, NLWeb, - llms.txt) within five minutes. The partner is responsible for ensuring the - business has consented to this provisioning and that the data provided is - accurate. - - Idempotent on `Idempotency-Key` for 24 hours. + """ + Provision a new dealer as child of the calling partner. Args: - postal_code: Italian 5-digit postal code. - - primary_domain: Root domain of the dealer's public website. - - province_code: Italian two-letter province code, e.g., `MI`, `RM`, `TO`. - - vat_number: Italian VAT number, 11 digits prefixed with `IT`. - - activate: If false, dealer is created in inactive state and does not appear in AI surfaces - until activated. - - contact_phone: E.164 format recommended. - - metadata: Free-form partner-supplied key-value pairs, max 16 keys, values max 500 chars. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -131,14 +110,16 @@ def create( "/v1/dealers", body=maybe_transform( { + "address": address, "business_name": business_name, + "city": city, "contact_email": contact_email, + "contact_phone": contact_phone, "postal_code": postal_code, "primary_domain": primary_domain, "province_code": province_code, "vat_number": vat_number, "activate": activate, - "contact_phone": contact_phone, "metadata": metadata, }, dealer_create_params.DealerCreateParams, @@ -160,8 +141,9 @@ def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DealerDetail: - """ - Get a dealer's full detail + """Fetch a dealer's full detail. + + ACL-protected. Args: extra_headers: Send extra headers @@ -186,13 +168,15 @@ def update( self, dealer_id: str, *, - business_name: str | Omit = omit, - contact_email: str | Omit = omit, - contact_phone: str | Omit = omit, - metadata: Dict[str, str] | Omit = omit, - postal_code: str | Omit = omit, - province_code: str | Omit = omit, - status: Literal["active", "inactive"] | Omit = omit, + address: Optional[str] | Omit = omit, + business_name: Optional[str] | Omit = omit, + city: Optional[str] | Omit = omit, + contact_email: Optional[str] | Omit = omit, + contact_phone: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + postal_code: Optional[str] | Omit = omit, + province_code: Optional[str] | Omit = omit, + status: Optional[Literal["active", "inactive"]] | Omit = omit, idempotency_key: str | 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. @@ -201,16 +185,11 @@ def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DealerDetail: - """Partial update of dealer fields. + """Update or toggle status. - Only provided fields are modified. Setting - `status: "inactive"` removes the dealer from the cross-network AI-citation - surfaces within five minutes. Reactivation: send `status: "active"`. + Inactive dealers drop from AI surfaces within 5 min. Args: - status: Toggle activation. Inactive dealers are removed from AI surfaces within 5 - minutes. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -226,7 +205,9 @@ def update( path_template("/v1/dealers/{dealer_id}", dealer_id=dealer_id), body=maybe_transform( { + "address": address, "business_name": business_name, + "city": city, "contact_email": contact_email, "contact_phone": contact_phone, "metadata": metadata, @@ -245,7 +226,7 @@ def update( def list( self, *, - cursor: str | Omit = omit, + cursor: Optional[str] | Omit = omit, limit: int | Omit = omit, status: Literal["active", "inactive", "all"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -255,17 +236,11 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DealerListResponse: - """ - Returns a cursor-paginated list of dealers belonging to the calling partner. - Default ordering: most recently created first. - - Args: - cursor: Opaque pagination cursor from a previous response's `next_cursor`. + """List dealers owned by the calling partner. - limit: Maximum number of items to return. - - status: Filter by dealer status. + Cursor-paginated. + Args: extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -304,12 +279,9 @@ def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """Marks the dealer as deleted while preserving the audit trail. + """Soft-delete. - Unlike - deactivation (`PATCH status=inactive`), a deleted dealer cannot be reactivated - by the partner — re-creation requires DealerMAX support. Use deactivation for - reversible suspensions. + Audit trail retained; reactivation requires DealerMAX support. Args: extra_headers: Send extra headers @@ -337,7 +309,6 @@ class AsyncDealersResource(AsyncAPIResource): @cached_property def nlt_settings(self) -> AsyncNltSettingsResource: - """Per-dealer NLT economics: agency markup percent and three down-payment tiers.""" return AsyncNltSettingsResource(self._client) @cached_property @@ -366,14 +337,16 @@ def with_streaming_response(self) -> AsyncDealersResourceWithStreamingResponse: async def create( self, *, + address: str, business_name: str, + city: str, contact_email: str, + contact_phone: str, postal_code: str, primary_domain: str, province_code: str, vat_number: str, activate: bool | Omit = omit, - contact_phone: str | Omit = omit, metadata: Dict[str, str] | Omit = omit, idempotency_key: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -383,32 +356,10 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DealerDetail: - """Creates a new dealer as a child of the calling partner account. - - The dealer is - indexed in the cross-network AI-citation surfaces (MCP, Custom GPT, NLWeb, - llms.txt) within five minutes. The partner is responsible for ensuring the - business has consented to this provisioning and that the data provided is - accurate. - - Idempotent on `Idempotency-Key` for 24 hours. + """ + Provision a new dealer as child of the calling partner. Args: - postal_code: Italian 5-digit postal code. - - primary_domain: Root domain of the dealer's public website. - - province_code: Italian two-letter province code, e.g., `MI`, `RM`, `TO`. - - vat_number: Italian VAT number, 11 digits prefixed with `IT`. - - activate: If false, dealer is created in inactive state and does not appear in AI surfaces - until activated. - - contact_phone: E.164 format recommended. - - metadata: Free-form partner-supplied key-value pairs, max 16 keys, values max 500 chars. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -422,14 +373,16 @@ async def create( "/v1/dealers", body=await async_maybe_transform( { + "address": address, "business_name": business_name, + "city": city, "contact_email": contact_email, + "contact_phone": contact_phone, "postal_code": postal_code, "primary_domain": primary_domain, "province_code": province_code, "vat_number": vat_number, "activate": activate, - "contact_phone": contact_phone, "metadata": metadata, }, dealer_create_params.DealerCreateParams, @@ -451,8 +404,9 @@ async def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DealerDetail: - """ - Get a dealer's full detail + """Fetch a dealer's full detail. + + ACL-protected. Args: extra_headers: Send extra headers @@ -477,13 +431,15 @@ async def update( self, dealer_id: str, *, - business_name: str | Omit = omit, - contact_email: str | Omit = omit, - contact_phone: str | Omit = omit, - metadata: Dict[str, str] | Omit = omit, - postal_code: str | Omit = omit, - province_code: str | Omit = omit, - status: Literal["active", "inactive"] | Omit = omit, + address: Optional[str] | Omit = omit, + business_name: Optional[str] | Omit = omit, + city: Optional[str] | Omit = omit, + contact_email: Optional[str] | Omit = omit, + contact_phone: Optional[str] | Omit = omit, + metadata: Optional[Dict[str, str]] | Omit = omit, + postal_code: Optional[str] | Omit = omit, + province_code: Optional[str] | Omit = omit, + status: Optional[Literal["active", "inactive"]] | Omit = omit, idempotency_key: str | 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. @@ -492,16 +448,11 @@ async def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DealerDetail: - """Partial update of dealer fields. + """Update or toggle status. - Only provided fields are modified. Setting - `status: "inactive"` removes the dealer from the cross-network AI-citation - surfaces within five minutes. Reactivation: send `status: "active"`. + Inactive dealers drop from AI surfaces within 5 min. Args: - status: Toggle activation. Inactive dealers are removed from AI surfaces within 5 - minutes. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -517,7 +468,9 @@ async def update( path_template("/v1/dealers/{dealer_id}", dealer_id=dealer_id), body=await async_maybe_transform( { + "address": address, "business_name": business_name, + "city": city, "contact_email": contact_email, "contact_phone": contact_phone, "metadata": metadata, @@ -536,7 +489,7 @@ async def update( async def list( self, *, - cursor: str | Omit = omit, + cursor: Optional[str] | Omit = omit, limit: int | Omit = omit, status: Literal["active", "inactive", "all"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -546,17 +499,11 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DealerListResponse: - """ - Returns a cursor-paginated list of dealers belonging to the calling partner. - Default ordering: most recently created first. - - Args: - cursor: Opaque pagination cursor from a previous response's `next_cursor`. + """List dealers owned by the calling partner. - limit: Maximum number of items to return. - - status: Filter by dealer status. + Cursor-paginated. + Args: extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -595,12 +542,9 @@ async def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """Marks the dealer as deleted while preserving the audit trail. + """Soft-delete. - Unlike - deactivation (`PATCH status=inactive`), a deleted dealer cannot be reactivated - by the partner — re-creation requires DealerMAX support. Use deactivation for - reversible suspensions. + Audit trail retained; reactivation requires DealerMAX support. Args: extra_headers: Send extra headers @@ -645,7 +589,6 @@ def __init__(self, dealers: DealersResource) -> None: @cached_property def nlt_settings(self) -> NltSettingsResourceWithRawResponse: - """Per-dealer NLT economics: agency markup percent and three down-payment tiers.""" return NltSettingsResourceWithRawResponse(self._dealers.nlt_settings) @cached_property @@ -675,7 +618,6 @@ def __init__(self, dealers: AsyncDealersResource) -> None: @cached_property def nlt_settings(self) -> AsyncNltSettingsResourceWithRawResponse: - """Per-dealer NLT economics: agency markup percent and three down-payment tiers.""" return AsyncNltSettingsResourceWithRawResponse(self._dealers.nlt_settings) @cached_property @@ -705,7 +647,6 @@ def __init__(self, dealers: DealersResource) -> None: @cached_property def nlt_settings(self) -> NltSettingsResourceWithStreamingResponse: - """Per-dealer NLT economics: agency markup percent and three down-payment tiers.""" return NltSettingsResourceWithStreamingResponse(self._dealers.nlt_settings) @cached_property @@ -735,7 +676,6 @@ def __init__(self, dealers: AsyncDealersResource) -> None: @cached_property def nlt_settings(self) -> AsyncNltSettingsResourceWithStreamingResponse: - """Per-dealer NLT economics: agency markup percent and three down-payment tiers.""" return AsyncNltSettingsResourceWithStreamingResponse(self._dealers.nlt_settings) @cached_property diff --git a/src/partnermax/resources/dealers/nlt/nlt.py b/src/partnermax/resources/dealers/nlt/nlt.py index 31938f7..46dafea 100644 --- a/src/partnermax/resources/dealers/nlt/nlt.py +++ b/src/partnermax/resources/dealers/nlt/nlt.py @@ -19,9 +19,6 @@ class NltResource(SyncAPIResource): @cached_property def offers(self) -> OffersResource: - """ - Dealer-aware reads of the shared NLT catalog — pricing reflects the dealer's markup and down-payment tiers. - """ return OffersResource(self._client) @cached_property @@ -47,9 +44,6 @@ def with_streaming_response(self) -> NltResourceWithStreamingResponse: class AsyncNltResource(AsyncAPIResource): @cached_property def offers(self) -> AsyncOffersResource: - """ - Dealer-aware reads of the shared NLT catalog — pricing reflects the dealer's markup and down-payment tiers. - """ return AsyncOffersResource(self._client) @cached_property @@ -78,9 +72,6 @@ def __init__(self, nlt: NltResource) -> None: @cached_property def offers(self) -> OffersResourceWithRawResponse: - """ - Dealer-aware reads of the shared NLT catalog — pricing reflects the dealer's markup and down-payment tiers. - """ return OffersResourceWithRawResponse(self._nlt.offers) @@ -90,9 +81,6 @@ def __init__(self, nlt: AsyncNltResource) -> None: @cached_property def offers(self) -> AsyncOffersResourceWithRawResponse: - """ - Dealer-aware reads of the shared NLT catalog — pricing reflects the dealer's markup and down-payment tiers. - """ return AsyncOffersResourceWithRawResponse(self._nlt.offers) @@ -102,9 +90,6 @@ def __init__(self, nlt: NltResource) -> None: @cached_property def offers(self) -> OffersResourceWithStreamingResponse: - """ - Dealer-aware reads of the shared NLT catalog — pricing reflects the dealer's markup and down-payment tiers. - """ return OffersResourceWithStreamingResponse(self._nlt.offers) @@ -114,7 +99,4 @@ def __init__(self, nlt: AsyncNltResource) -> None: @cached_property def offers(self) -> AsyncOffersResourceWithStreamingResponse: - """ - Dealer-aware reads of the shared NLT catalog — pricing reflects the dealer's markup and down-payment tiers. - """ return AsyncOffersResourceWithStreamingResponse(self._nlt.offers) diff --git a/src/partnermax/resources/dealers/nlt/offers.py b/src/partnermax/resources/dealers/nlt/offers.py index 40c7cbf..e4bf72e 100644 --- a/src/partnermax/resources/dealers/nlt/offers.py +++ b/src/partnermax/resources/dealers/nlt/offers.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Literal +from typing import Optional import httpx @@ -25,10 +25,6 @@ class OffersResource(SyncAPIResource): - """ - Dealer-aware reads of the shared NLT catalog — pricing reflects the dealer's markup and down-payment tiers. - """ - @cached_property def with_raw_response(self) -> OffersResourceWithRawResponse: """ @@ -60,11 +56,10 @@ def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> OfferRetrieveResponse: - """ - Returns the full detail of one NLT offer including the 18-quotation matrix (3 - durations × 6 km/year tiers) with prices computed for each of the dealer's three - down-payment tiers — 54 displayed price points total — plus included services, - optional accessories, image gallery, and the dealer business card. + """Full offer detail. + + Payload shape mirrors apimax MCP `get_nlt_offer_details` + bit-for-bit (mcp_server.py:1546-1606). Args: extra_headers: Send extra headers @@ -91,14 +86,14 @@ def list( self, dealer_id: str, *, - brand: str | Omit = omit, - canone_max_eur: int | Omit = omit, - cursor: str | Omit = omit, - duration_months: Literal[24, 36, 48] | Omit = omit, - fuel_type: str | Omit = omit, - km_per_year: Literal[10000, 15000, 20000, 25000, 30000, 40000] | Omit = omit, + brand: Optional[str] | Omit = omit, + canone_max_eur: Optional[int] | Omit = omit, + cursor: Optional[str] | Omit = omit, + duration_months: Optional[int] | Omit = omit, + fuel_type: Optional[str] | Omit = omit, + km_per_year: Optional[int] | Omit = omit, limit: int | Omit = omit, - segment: str | Omit = omit, + segment: Optional[str] | 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, @@ -106,24 +101,22 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> OfferListResponse: - """ - Returns a cursor-paginated list of NLT offers available to the specified dealer, - with monthly canon already repriced using the dealer's markup and down-payment - tiers. + """Listing of NLT offers with monthly canon repriced for this dealer. - Args: - brand: Filter by brand name, case-insensitive (e.g., `Fiat`). - - canone_max_eur: Upper bound on displayed monthly canon (EUR). + Strategy: - cursor: Opaque pagination cursor. + 1. - fuel_type: Raw Italian label (case-insensitive ILIKE match). Examples: "Benzina", "Diesel", - "Ibrido benzina", "Ibrido diesel", "Elettrica", "GPL", "Metano". - - segment: Raw Italian label (case-insensitive ILIKE substring match). Examples: "SUV - piccoli", "SUV medi", "Superiori", "Medie", "Utilitarie". + Resolve + ACL the dealer. + 2. Pull at most `limit + 1` offers from the catalog after the cursor. The extra + row lets us know if there's a next page without a second COUNT(\\**) query. + 3. Apply text/enum filters server-side via SQL where possible (brand, segment, + fuel) and the numeric `canone_max_eur` filter in Python after the pricing + pass (the DB has no "displayed canon" column; we synthesize it per dealer). + 4. For each surviving offer, price the (duration, km) cells the caller filtered + to (if specified) or all 18, pick the cheapest cell as the headline. + Args: extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -160,10 +153,6 @@ def list( class AsyncOffersResource(AsyncAPIResource): - """ - Dealer-aware reads of the shared NLT catalog — pricing reflects the dealer's markup and down-payment tiers. - """ - @cached_property def with_raw_response(self) -> AsyncOffersResourceWithRawResponse: """ @@ -195,11 +184,10 @@ async def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> OfferRetrieveResponse: - """ - Returns the full detail of one NLT offer including the 18-quotation matrix (3 - durations × 6 km/year tiers) with prices computed for each of the dealer's three - down-payment tiers — 54 displayed price points total — plus included services, - optional accessories, image gallery, and the dealer business card. + """Full offer detail. + + Payload shape mirrors apimax MCP `get_nlt_offer_details` + bit-for-bit (mcp_server.py:1546-1606). Args: extra_headers: Send extra headers @@ -226,14 +214,14 @@ async def list( self, dealer_id: str, *, - brand: str | Omit = omit, - canone_max_eur: int | Omit = omit, - cursor: str | Omit = omit, - duration_months: Literal[24, 36, 48] | Omit = omit, - fuel_type: str | Omit = omit, - km_per_year: Literal[10000, 15000, 20000, 25000, 30000, 40000] | Omit = omit, + brand: Optional[str] | Omit = omit, + canone_max_eur: Optional[int] | Omit = omit, + cursor: Optional[str] | Omit = omit, + duration_months: Optional[int] | Omit = omit, + fuel_type: Optional[str] | Omit = omit, + km_per_year: Optional[int] | Omit = omit, limit: int | Omit = omit, - segment: str | Omit = omit, + segment: Optional[str] | 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, @@ -241,24 +229,22 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> OfferListResponse: - """ - Returns a cursor-paginated list of NLT offers available to the specified dealer, - with monthly canon already repriced using the dealer's markup and down-payment - tiers. + """Listing of NLT offers with monthly canon repriced for this dealer. - Args: - brand: Filter by brand name, case-insensitive (e.g., `Fiat`). - - canone_max_eur: Upper bound on displayed monthly canon (EUR). + Strategy: - cursor: Opaque pagination cursor. + 1. - fuel_type: Raw Italian label (case-insensitive ILIKE match). Examples: "Benzina", "Diesel", - "Ibrido benzina", "Ibrido diesel", "Elettrica", "GPL", "Metano". - - segment: Raw Italian label (case-insensitive ILIKE substring match). Examples: "SUV - piccoli", "SUV medi", "Superiori", "Medie", "Utilitarie". + Resolve + ACL the dealer. + 2. Pull at most `limit + 1` offers from the catalog after the cursor. The extra + row lets us know if there's a next page without a second COUNT(\\**) query. + 3. Apply text/enum filters server-side via SQL where possible (brand, segment, + fuel) and the numeric `canone_max_eur` filter in Python after the pricing + pass (the DB has no "displayed canon" column; we synthesize it per dealer). + 4. For each surviving offer, price the (duration, km) cells the caller filtered + to (if specified) or all 18, pick the cheapest cell as the headline. + Args: extra_headers: Send extra headers extra_query: Add additional query parameters to the request diff --git a/src/partnermax/resources/dealers/nlt_settings.py b/src/partnermax/resources/dealers/nlt_settings.py index d7ce19d..7e50307 100644 --- a/src/partnermax/resources/dealers/nlt_settings.py +++ b/src/partnermax/resources/dealers/nlt_settings.py @@ -8,7 +8,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -26,8 +26,6 @@ class NltSettingsResource(SyncAPIResource): - """Per-dealer NLT economics: agency markup percent and three down-payment tiers.""" - @cached_property def with_raw_response(self) -> NltSettingsResourceWithRawResponse: """ @@ -59,7 +57,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> NltSettings: """ - Get current NLT economics for a dealer + Return current NLT economics for the dealer. Args: extra_headers: Send extra headers @@ -86,10 +84,9 @@ def update( *, agency_markup_percent: float, down_payment_tiers: DownPaymentTiersParam, - image_mode: Literal["branded", "scenario_locked", "scenario_seasonal"], currency: Literal["EUR"] | Omit = omit, + image_mode: Literal["branded", "scenario_locked", "scenario_seasonal"] | Omit = omit, image_scenario_locked: Optional[Literal["mediterraneo", "cortina", "milano", "showroom"]] | Omit = omit, - idempotency_key: str | 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, @@ -98,31 +95,40 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> NltSettings: """ - Sets the dealer's agency markup percent (0–10) and three down-payment tiers (low - / medium / high). Each tier carries `percent_of_list (0–100)` + - `fixed_eur (≥0)`; the final EUR per tier is offer-dependent - (`listino_imponibile * pct + eur`). Changes propagate to the cross-network AI - surfaces within five minutes. + Set markup percent (0-10) and three down-payment tiers (strictly ascending). + + Validation: + + - `agency_markup_percent` ∈ [0, 10] (Pydantic). + - `down_payment_tiers.{low,medium,high}` each + `{percent_of_list (0–100), fixed_eur (≥0)}`. No strict-ascending check — the + final EUR per tier is offer-dependent (`listino_imponibile * pct + eur`). - The displayed monthly canon for an offer is computed as: + Persistence: - ``` - listino_imponibile = prezzo_listino / 1.22 - provvigione = listino_imponibile × (agency_markup_percent / 100) - anticipo_tier_eur = listino_imponibile × (tier.percent_of_list / 100) + tier.fixed_eur - canon = base_canon + provvigione / duration_months − anticipo_tier_eur / duration_months - if offer.private_only: canon *= 1.22 - ``` + - `agency_markup_percent` → `dealer_public.nlt_agency_percent` (rounded to int; + live column is `Integer NOT NULL DEFAULT 2`). + - `down_payment_tiers` → `dealer_public.nlt_anticipi_config` JSONB, stored in + apimax shape `[{"pct": <0..1>, "eur": }, ...]`. The partner-facing + `percent_of_list` (0–100) is divided by 100 to keep the column byte-compatible + with the DealerMAX UI calculator that reads the same JSONB. - VAT treatment is a property of each offer (`NltOfferDetail.private_only` / - `NltOfferSummary.vat_treatment`), not of the dealer. + There is NO `vat_treatment` field: VAT is per-offer (`nlt_offerte.solo_privati`) + in the canonical DataMax pricing model, not per-dealer. The offer detail + endpoint surfaces it per row instead. + + The Idempotency middleware handles `Idempotency-Key` replay outside this + handler; we just need to make the write itself idempotent at the row level (a + re-applied identical PATCH is a no-op by construction). Args: - down_payment_tiers: Three down-payment scenarios (basso / medio / alto). Each tier carries - `{percent_of_list (0–100), fixed_eur (≥0)}`. No strict-ascending check — the - final EUR per tier is offer-dependent (`listino_imponibile * pct + eur`). + down_payment_tiers: Three down-payment scenarios (basso / medio / alto). - image_scenario_locked: Required when `image_mode='scenario_locked'`; must be null otherwise. + No strict-ascending validation: the final EUR amount depends on the offer's list + price (`tier.percent_of_list / 100 * listino_imponibile + tier.fixed_eur`), so a + tier that looks larger by % can produce a smaller EUR on cheap vehicles. Label + semantics (low/medium/high) are advisory — apimax/DealerMAX UI treats the 3 + positions as opaque slots ordered by intent. extra_headers: Send extra headers @@ -134,15 +140,14 @@ def update( """ if not dealer_id: raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") - extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} return self._patch( path_template("/v1/dealers/{dealer_id}/nlt-settings", dealer_id=dealer_id), body=maybe_transform( { "agency_markup_percent": agency_markup_percent, "down_payment_tiers": down_payment_tiers, - "image_mode": image_mode, "currency": currency, + "image_mode": image_mode, "image_scenario_locked": image_scenario_locked, }, nlt_setting_update_params.NltSettingUpdateParams, @@ -155,8 +160,6 @@ def update( class AsyncNltSettingsResource(AsyncAPIResource): - """Per-dealer NLT economics: agency markup percent and three down-payment tiers.""" - @cached_property def with_raw_response(self) -> AsyncNltSettingsResourceWithRawResponse: """ @@ -188,7 +191,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> NltSettings: """ - Get current NLT economics for a dealer + Return current NLT economics for the dealer. Args: extra_headers: Send extra headers @@ -215,10 +218,9 @@ async def update( *, agency_markup_percent: float, down_payment_tiers: DownPaymentTiersParam, - image_mode: Literal["branded", "scenario_locked", "scenario_seasonal"], currency: Literal["EUR"] | Omit = omit, + image_mode: Literal["branded", "scenario_locked", "scenario_seasonal"] | Omit = omit, image_scenario_locked: Optional[Literal["mediterraneo", "cortina", "milano", "showroom"]] | Omit = omit, - idempotency_key: str | 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, @@ -227,31 +229,40 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> NltSettings: """ - Sets the dealer's agency markup percent (0–10) and three down-payment tiers (low - / medium / high). Each tier carries `percent_of_list (0–100)` + - `fixed_eur (≥0)`; the final EUR per tier is offer-dependent - (`listino_imponibile * pct + eur`). Changes propagate to the cross-network AI - surfaces within five minutes. + Set markup percent (0-10) and three down-payment tiers (strictly ascending). + + Validation: + + - `agency_markup_percent` ∈ [0, 10] (Pydantic). + - `down_payment_tiers.{low,medium,high}` each + `{percent_of_list (0–100), fixed_eur (≥0)}`. No strict-ascending check — the + final EUR per tier is offer-dependent (`listino_imponibile * pct + eur`). - The displayed monthly canon for an offer is computed as: + Persistence: - ``` - listino_imponibile = prezzo_listino / 1.22 - provvigione = listino_imponibile × (agency_markup_percent / 100) - anticipo_tier_eur = listino_imponibile × (tier.percent_of_list / 100) + tier.fixed_eur - canon = base_canon + provvigione / duration_months − anticipo_tier_eur / duration_months - if offer.private_only: canon *= 1.22 - ``` + - `agency_markup_percent` → `dealer_public.nlt_agency_percent` (rounded to int; + live column is `Integer NOT NULL DEFAULT 2`). + - `down_payment_tiers` → `dealer_public.nlt_anticipi_config` JSONB, stored in + apimax shape `[{"pct": <0..1>, "eur": }, ...]`. The partner-facing + `percent_of_list` (0–100) is divided by 100 to keep the column byte-compatible + with the DealerMAX UI calculator that reads the same JSONB. - VAT treatment is a property of each offer (`NltOfferDetail.private_only` / - `NltOfferSummary.vat_treatment`), not of the dealer. + There is NO `vat_treatment` field: VAT is per-offer (`nlt_offerte.solo_privati`) + in the canonical DataMax pricing model, not per-dealer. The offer detail + endpoint surfaces it per row instead. + + The Idempotency middleware handles `Idempotency-Key` replay outside this + handler; we just need to make the write itself idempotent at the row level (a + re-applied identical PATCH is a no-op by construction). Args: - down_payment_tiers: Three down-payment scenarios (basso / medio / alto). Each tier carries - `{percent_of_list (0–100), fixed_eur (≥0)}`. No strict-ascending check — the - final EUR per tier is offer-dependent (`listino_imponibile * pct + eur`). + down_payment_tiers: Three down-payment scenarios (basso / medio / alto). - image_scenario_locked: Required when `image_mode='scenario_locked'`; must be null otherwise. + No strict-ascending validation: the final EUR amount depends on the offer's list + price (`tier.percent_of_list / 100 * listino_imponibile + tier.fixed_eur`), so a + tier that looks larger by % can produce a smaller EUR on cheap vehicles. Label + semantics (low/medium/high) are advisory — apimax/DealerMAX UI treats the 3 + positions as opaque slots ordered by intent. extra_headers: Send extra headers @@ -263,15 +274,14 @@ async def update( """ if not dealer_id: raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") - extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} return await self._patch( path_template("/v1/dealers/{dealer_id}/nlt-settings", dealer_id=dealer_id), body=await async_maybe_transform( { "agency_markup_percent": agency_markup_percent, "down_payment_tiers": down_payment_tiers, - "image_mode": image_mode, "currency": currency, + "image_mode": image_mode, "image_scenario_locked": image_scenario_locked, }, nlt_setting_update_params.NltSettingUpdateParams, diff --git a/src/partnermax/resources/keys.py b/src/partnermax/resources/keys.py index 8f9e42b..b340bd6 100644 --- a/src/partnermax/resources/keys.py +++ b/src/partnermax/resources/keys.py @@ -60,11 +60,11 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> KeyListResponse: - """Returns metadata for all active keys belonging to the calling partner. + """ + List metadata for keys owned by the calling partner. - Key - material is never returned — only the prefix (first 8 characters) for safe - logging and identification. + Returns both active and revoked keys — partners can audit revoked keys via the + `is_active` flag. Key material is never returned. """ return self._get( "/v1/keys", @@ -78,7 +78,7 @@ def issue( self, *, label: str, - expires_at: Union[str, datetime] | Omit = omit, + expires_at: Union[str, datetime, None] | Omit = omit, idempotency_key: str | 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. @@ -87,14 +87,14 @@ def issue( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> KeyIssueResponse: - """Creates a new API key for the calling partner. + """Issue a new API key. + + Plaintext returned exactly once. - The key material is returned in - plaintext in the response and is never retrievable again — store it securely on - first receipt. Must be called with an existing API key that has the - `can_issue_keys` capability (the initial key issued by DealerMAX support has - this capability by default; rotated keys inherit it unless explicitly scoped - down). + Capability gate: caller's key must hold `can_issue_keys`. The bootstrap key + handed to a partner by DealerMAX support carries this capability; keys minted + here inherit the _same_ capabilities as the caller, so partners can never + accidentally widen their own scope. Args: label: Human-readable identifier for this key, used for safe logging. @@ -136,11 +136,9 @@ def revoke( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """Immediately invalidates the specified key. + """Revoke a key. - Any in-flight requests using this key - will continue until completion; subsequent requests will receive 401 - invalid_api_key. Revocation is logged in the audit trail. + Cannot revoke the key currently authenticating the request. Args: extra_headers: Send extra headers @@ -198,11 +196,11 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> KeyListResponse: - """Returns metadata for all active keys belonging to the calling partner. + """ + List metadata for keys owned by the calling partner. - Key - material is never returned — only the prefix (first 8 characters) for safe - logging and identification. + Returns both active and revoked keys — partners can audit revoked keys via the + `is_active` flag. Key material is never returned. """ return await self._get( "/v1/keys", @@ -216,7 +214,7 @@ async def issue( self, *, label: str, - expires_at: Union[str, datetime] | Omit = omit, + expires_at: Union[str, datetime, None] | Omit = omit, idempotency_key: str | 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. @@ -225,14 +223,14 @@ async def issue( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> KeyIssueResponse: - """Creates a new API key for the calling partner. + """Issue a new API key. + + Plaintext returned exactly once. - The key material is returned in - plaintext in the response and is never retrievable again — store it securely on - first receipt. Must be called with an existing API key that has the - `can_issue_keys` capability (the initial key issued by DealerMAX support has - this capability by default; rotated keys inherit it unless explicitly scoped - down). + Capability gate: caller's key must hold `can_issue_keys`. The bootstrap key + handed to a partner by DealerMAX support carries this capability; keys minted + here inherit the _same_ capabilities as the caller, so partners can never + accidentally widen their own scope. Args: label: Human-readable identifier for this key, used for safe logging. @@ -274,11 +272,9 @@ async def revoke( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> None: - """Immediately invalidates the specified key. + """Revoke a key. - Any in-flight requests using this key - will continue until completion; subsequent requests will receive 401 - invalid_api_key. Revocation is logged in the audit trail. + Cannot revoke the key currently authenticating the request. Args: extra_headers: Send extra headers diff --git a/src/partnermax/types/dealer_create_params.py b/src/partnermax/types/dealer_create_params.py index 6d77999..91e5534 100644 --- a/src/partnermax/types/dealer_create_params.py +++ b/src/partnermax/types/dealer_create_params.py @@ -11,32 +11,26 @@ class DealerCreateParams(TypedDict, total=False): + address: Required[str] + business_name: Required[str] + city: Required[str] + contact_email: Required[str] + contact_phone: Required[str] + postal_code: Required[str] - """Italian 5-digit postal code.""" primary_domain: Required[str] - """Root domain of the dealer's public website.""" province_code: Required[str] - """Italian two-letter province code, e.g., `MI`, `RM`, `TO`.""" vat_number: Required[str] - """Italian VAT number, 11 digits prefixed with `IT`.""" activate: bool - """ - If false, dealer is created in inactive state and does not appear in AI surfaces - until activated. - """ - - contact_phone: str - """E.164 format recommended.""" metadata: Dict[str, str] - """Free-form partner-supplied key-value pairs, max 16 keys, values max 500 chars.""" idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] diff --git a/src/partnermax/types/dealer_detail.py b/src/partnermax/types/dealer_detail.py index 3831c99..b80fcb9 100644 --- a/src/partnermax/types/dealer_detail.py +++ b/src/partnermax/types/dealer_detail.py @@ -1,18 +1,20 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Dict, Optional +from datetime import datetime +from typing_extensions import Literal from .._models import BaseModel -from .dealer_summary import DealerSummary -from .dealers.nlt_settings import NltSettings -__all__ = ["DealerDetail", "DealerDetailIndexedInSurfaces"] +__all__ = ["DealerDetail", "IndexedInSurfaces"] -class DealerDetailIndexedInSurfaces(BaseModel): - """Live indexing state for each cross-network AI surface. +class IndexedInSurfaces(BaseModel): + """Per-surface AI indexing state. - Values may be `false` immediately after provisioning; reaches `true` within five minutes. + All values may be ``false`` immediately + after provisioning; reconciliation by ``azurenet-engine`` flips them within + five minutes. """ custom_gpt: Optional[bool] = None @@ -24,29 +26,46 @@ class DealerDetailIndexedInSurfaces(BaseModel): nlweb: Optional[bool] = None -class DealerDetail(DealerSummary): - contact_email: Optional[str] = None +class DealerDetail(BaseModel): + """Full dealer payload used by single-resource and write endpoints.""" - contact_phone: Optional[str] = None + address: str - indexed_in_surfaces: Optional[DealerDetailIndexedInSurfaces] = None - """Live indexing state for each cross-network AI surface. + business_name: str - Values may be `false` immediately after provisioning; reaches `true` within five - minutes. - """ + city: str - metadata: Optional[Dict[str, str]] = None + contact_email: str + + created_at: datetime + + dealer_id: str + + nlt_enabled: bool + + partner_id: str + + postal_code: str - nlt_settings: Optional[NltSettings] = None - """Dealer-level NLT economics + image rendering preferences. + primary_domain: str - VAT treatment is NOT a dealer-level field — it is a property of the offer (see - `NltOfferSummary.vat_treatment`). + province_code: str + + status: Literal["active", "inactive", "deleted"] + + vat_number: str + + contact_phone: Optional[str] = None + + indexed_in_surfaces: Optional[IndexedInSurfaces] = None + """Per-surface AI indexing state. + + All values may be `false` immediately after provisioning; reconciliation by + `azurenet-engine` flips them within five minutes. """ - partner_id: Optional[str] = None + last_active_at: Optional[datetime] = None - postal_code: Optional[str] = None + metadata: Optional[Dict[str, str]] = None - vat_number: Optional[str] = None + nlt_settings: Optional[Dict[str, object]] = None diff --git a/src/partnermax/types/dealer_list_params.py b/src/partnermax/types/dealer_list_params.py index aa81405..f54d641 100644 --- a/src/partnermax/types/dealer_list_params.py +++ b/src/partnermax/types/dealer_list_params.py @@ -2,17 +2,15 @@ from __future__ import annotations +from typing import Optional from typing_extensions import Literal, TypedDict __all__ = ["DealerListParams"] class DealerListParams(TypedDict, total=False): - cursor: str - """Opaque pagination cursor from a previous response's `next_cursor`.""" + cursor: Optional[str] limit: int - """Maximum number of items to return.""" status: Literal["active", "inactive", "all"] - """Filter by dealer status.""" diff --git a/src/partnermax/types/dealer_list_response.py b/src/partnermax/types/dealer_list_response.py index bf17203..436dc2e 100644 --- a/src/partnermax/types/dealer_list_response.py +++ b/src/partnermax/types/dealer_list_response.py @@ -9,9 +9,10 @@ class DealerListResponse(BaseModel): + """Response envelope for ``GET /v1/dealers``.""" + data: List[DealerSummary] has_more: bool next_cursor: Optional[str] = None - """Pass as `cursor` to retrieve next page; null when no more pages.""" diff --git a/src/partnermax/types/dealer_summary.py b/src/partnermax/types/dealer_summary.py index e65c1a1..69ecd6b 100644 --- a/src/partnermax/types/dealer_summary.py +++ b/src/partnermax/types/dealer_summary.py @@ -10,6 +10,8 @@ class DealerSummary(BaseModel): + """Compact dealer payload used by list endpoints.""" + business_name: str created_at: datetime diff --git a/src/partnermax/types/dealer_update_params.py b/src/partnermax/types/dealer_update_params.py index 885f7e1..251f5cf 100644 --- a/src/partnermax/types/dealer_update_params.py +++ b/src/partnermax/types/dealer_update_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict +from typing import Dict, Optional from typing_extensions import Literal, Annotated, TypedDict from .._utils import PropertyInfo @@ -11,22 +11,22 @@ class DealerUpdateParams(TypedDict, total=False): - business_name: str + address: Optional[str] - contact_email: str + business_name: Optional[str] - contact_phone: str + city: Optional[str] - metadata: Dict[str, str] + contact_email: Optional[str] - postal_code: str + contact_phone: Optional[str] - province_code: str + metadata: Optional[Dict[str, str]] - status: Literal["active", "inactive"] - """Toggle activation. + postal_code: Optional[str] - Inactive dealers are removed from AI surfaces within 5 minutes. - """ + province_code: Optional[str] + + status: Optional[Literal["active", "inactive"]] idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] diff --git a/src/partnermax/types/dealers/down_payment_tiers.py b/src/partnermax/types/dealers/down_payment_tiers.py index 7cf52ce..eb528d8 100644 --- a/src/partnermax/types/dealers/down_payment_tiers.py +++ b/src/partnermax/types/dealers/down_payment_tiers.py @@ -6,54 +6,81 @@ class High(BaseModel): - """ - One down-payment tier — `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. Persisted in apimax shape `{"pct": <0..1>, "eur": }` on `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to + a deal is `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number + (UI-friendly: write `12.5`, not `0.125`); the router persists + `pct = percent_of_list / 100` to stay byte-compatible with the + DealerMAX UI calculator. """ fixed_eur: int """Flat EUR component added on top of the percentage (e.g. - promo `0% + 500€ fissi`). Whole euros only. + promo `0% + 500 EUR fissi`). Whole euros only. """ percent_of_list: float - """Percent of the IVA-excluded list price applied as down payment for this tier. + """Percentage of the IVA-excluded list price applied as down payment for this tier. Range 0–100. Typical defaults: 0 (low), 12.5 (medium), 25 (high). """ class Low(BaseModel): - """ - One down-payment tier — `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. Persisted in apimax shape `{"pct": <0..1>, "eur": }` on `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to + a deal is `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number + (UI-friendly: write `12.5`, not `0.125`); the router persists + `pct = percent_of_list / 100` to stay byte-compatible with the + DealerMAX UI calculator. """ fixed_eur: int """Flat EUR component added on top of the percentage (e.g. - promo `0% + 500€ fissi`). Whole euros only. + promo `0% + 500 EUR fissi`). Whole euros only. """ percent_of_list: float - """Percent of the IVA-excluded list price applied as down payment for this tier. + """Percentage of the IVA-excluded list price applied as down payment for this tier. Range 0–100. Typical defaults: 0 (low), 12.5 (medium), 25 (high). """ class Medium(BaseModel): - """ - One down-payment tier — `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. Persisted in apimax shape `{"pct": <0..1>, "eur": }` on `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to + a deal is `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number + (UI-friendly: write `12.5`, not `0.125`); the router persists + `pct = percent_of_list / 100` to stay byte-compatible with the + DealerMAX UI calculator. """ fixed_eur: int """Flat EUR component added on top of the percentage (e.g. - promo `0% + 500€ fissi`). Whole euros only. + promo `0% + 500 EUR fissi`). Whole euros only. """ percent_of_list: float - """Percent of the IVA-excluded list price applied as down payment for this tier. + """Percentage of the IVA-excluded list price applied as down payment for this tier. Range 0–100. Typical defaults: 0 (low), 12.5 (medium), 25 (high). """ @@ -62,29 +89,49 @@ class Medium(BaseModel): class DownPaymentTiers(BaseModel): """Three down-payment scenarios (basso / medio / alto). - Each tier carries `{percent_of_list (0–100), fixed_eur (≥0)}`. No strict-ascending check — the final EUR per tier is offer-dependent (`listino_imponibile * pct + eur`). + No strict-ascending validation: the final EUR amount depends on + the offer's list price (`tier.percent_of_list / 100 * + listino_imponibile + tier.fixed_eur`), so a tier that looks + larger by % can produce a smaller EUR on cheap vehicles. Label + semantics (low/medium/high) are advisory — apimax/DealerMAX UI + treats the 3 positions as opaque slots ordered by intent. """ high: High - """ - One down-payment tier — - `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. - Persisted in apimax shape `{"pct": <0..1>, "eur": }` on - `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is + `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write + `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay + byte-compatible with the DealerMAX UI calculator. """ low: Low - """ - One down-payment tier — - `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. - Persisted in apimax shape `{"pct": <0..1>, "eur": }` on - `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is + `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write + `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay + byte-compatible with the DealerMAX UI calculator. """ medium: Medium - """ - One down-payment tier — - `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. - Persisted in apimax shape `{"pct": <0..1>, "eur": }` on - `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is + `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write + `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay + byte-compatible with the DealerMAX UI calculator. """ diff --git a/src/partnermax/types/dealers/down_payment_tiers_param.py b/src/partnermax/types/dealers/down_payment_tiers_param.py index 9024da5..6c0d5fe 100644 --- a/src/partnermax/types/dealers/down_payment_tiers_param.py +++ b/src/partnermax/types/dealers/down_payment_tiers_param.py @@ -8,54 +8,81 @@ class High(TypedDict, total=False): - """ - One down-payment tier — `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. Persisted in apimax shape `{"pct": <0..1>, "eur": }` on `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to + a deal is `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number + (UI-friendly: write `12.5`, not `0.125`); the router persists + `pct = percent_of_list / 100` to stay byte-compatible with the + DealerMAX UI calculator. """ fixed_eur: Required[int] """Flat EUR component added on top of the percentage (e.g. - promo `0% + 500€ fissi`). Whole euros only. + promo `0% + 500 EUR fissi`). Whole euros only. """ percent_of_list: Required[float] - """Percent of the IVA-excluded list price applied as down payment for this tier. + """Percentage of the IVA-excluded list price applied as down payment for this tier. Range 0–100. Typical defaults: 0 (low), 12.5 (medium), 25 (high). """ class Low(TypedDict, total=False): - """ - One down-payment tier — `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. Persisted in apimax shape `{"pct": <0..1>, "eur": }` on `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to + a deal is `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number + (UI-friendly: write `12.5`, not `0.125`); the router persists + `pct = percent_of_list / 100` to stay byte-compatible with the + DealerMAX UI calculator. """ fixed_eur: Required[int] """Flat EUR component added on top of the percentage (e.g. - promo `0% + 500€ fissi`). Whole euros only. + promo `0% + 500 EUR fissi`). Whole euros only. """ percent_of_list: Required[float] - """Percent of the IVA-excluded list price applied as down payment for this tier. + """Percentage of the IVA-excluded list price applied as down payment for this tier. Range 0–100. Typical defaults: 0 (low), 12.5 (medium), 25 (high). """ class Medium(TypedDict, total=False): - """ - One down-payment tier — `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. Persisted in apimax shape `{"pct": <0..1>, "eur": }` on `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to + a deal is `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number + (UI-friendly: write `12.5`, not `0.125`); the router persists + `pct = percent_of_list / 100` to stay byte-compatible with the + DealerMAX UI calculator. """ fixed_eur: Required[int] """Flat EUR component added on top of the percentage (e.g. - promo `0% + 500€ fissi`). Whole euros only. + promo `0% + 500 EUR fissi`). Whole euros only. """ percent_of_list: Required[float] - """Percent of the IVA-excluded list price applied as down payment for this tier. + """Percentage of the IVA-excluded list price applied as down payment for this tier. Range 0–100. Typical defaults: 0 (low), 12.5 (medium), 25 (high). """ @@ -64,29 +91,49 @@ class Medium(TypedDict, total=False): class DownPaymentTiersParam(TypedDict, total=False): """Three down-payment scenarios (basso / medio / alto). - Each tier carries `{percent_of_list (0–100), fixed_eur (≥0)}`. No strict-ascending check — the final EUR per tier is offer-dependent (`listino_imponibile * pct + eur`). + No strict-ascending validation: the final EUR amount depends on + the offer's list price (`tier.percent_of_list / 100 * + listino_imponibile + tier.fixed_eur`), so a tier that looks + larger by % can produce a smaller EUR on cheap vehicles. Label + semantics (low/medium/high) are advisory — apimax/DealerMAX UI + treats the 3 positions as opaque slots ordered by intent. """ high: Required[High] - """ - One down-payment tier — - `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. - Persisted in apimax shape `{"pct": <0..1>, "eur": }` on - `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is + `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write + `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay + byte-compatible with the DealerMAX UI calculator. """ low: Required[Low] - """ - One down-payment tier — - `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. - Persisted in apimax shape `{"pct": <0..1>, "eur": }` on - `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is + `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write + `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay + byte-compatible with the DealerMAX UI calculator. """ medium: Required[Medium] - """ - One down-payment tier — - `final_eur = listino_imponibile * (percent_of_list / 100) + fixed_eur`. - Persisted in apimax shape `{"pct": <0..1>, "eur": }` on - `dealer_public.nlt_anticipi_config`. + """One down-payment tier — percent of list price + flat EUR. + + apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three + `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is + `listino_imponibile * pct + eur` (see + `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). + + Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write + `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay + byte-compatible with the DealerMAX UI calculator. """ diff --git a/src/partnermax/types/dealers/nlt/nlt_offer_summary.py b/src/partnermax/types/dealers/nlt/nlt_offer_summary.py index 0615904..2dc878a 100644 --- a/src/partnermax/types/dealers/nlt/nlt_offer_summary.py +++ b/src/partnermax/types/dealers/nlt/nlt_offer_summary.py @@ -9,62 +9,41 @@ class NltOfferSummary(BaseModel): + """Single row in the offers list. Pricing is dealer-aware. + + Field names: American English snake_case. Values: Italian raw, + apimax-aligned (`fuel_type: "Benzina"`, `segment: "SUV piccoli"`). + No enum normalization — apimax labels are surfaced verbatim, exactly + as the detail endpoint does, so the partner client sees the same + string in both listing and detail. + """ + brand: str dealer_id: str - """Dealer id (prefixed `dlr_`).""" - duration_months: Literal[36, 48, 60] - """Duration corresponding to the `monthly_canon_from_eur` quote.""" + duration_months: int - km_per_year_at_quote: Literal[10000, 15000, 20000, 25000, 30000, 40000] - """Km/year corresponding to the `monthly_canon_from_eur` quote.""" + km_per_year_at_quote: int model: str monthly_canon_from_eur: float - """ - Lowest displayed monthly canon across all duration / km / down-payment - combinations for this dealer. - """ offer_id: str - """Numeric `nlt_offerte.id_offerta` as string. - - Use as the path parameter for the detail endpoint. - """ slug: str - """Offer slug used in canonical URLs (`/noleggio-lungo-termine/{slug}`).""" vat_treatment: Literal["private", "business"] - """VAT treatment of this offer (not the dealer). - - `private` → `monthly_canon_from_eur` is VAT-inclusive (×1.22). `business` → - VAT-exclusive. Sourced from `nlt_offerte.solo_privati`. - """ canonical_url: Optional[str] = None - """ - Consumer-facing URL on the dealer's public site: - `https://{primary_domain}/noleggio-lungo-termine/{slug}`. Null when the dealer - has no site row. - """ fuel_type: Optional[str] = None - """Raw Italian label from `nlt_offerte.alimentazione` (e.g. - - "Benzina", "Ibrido diesel"). Apimax-aligned, no enum normalization. - """ has_promo: Optional[bool] = None image_url: Optional[str] = None segment: Optional[str] = None - """Raw Italian label from `nlt_offerte.segmento` (e.g. - - "SUV piccoli", "Superiori"). Apimax-aligned, no enum normalization. - """ trim: Optional[str] = None diff --git a/src/partnermax/types/dealers/nlt/offer_list_params.py b/src/partnermax/types/dealers/nlt/offer_list_params.py index e335d2b..765ada0 100644 --- a/src/partnermax/types/dealers/nlt/offer_list_params.py +++ b/src/partnermax/types/dealers/nlt/offer_list_params.py @@ -2,36 +2,25 @@ from __future__ import annotations -from typing_extensions import Literal, TypedDict +from typing import Optional +from typing_extensions import TypedDict __all__ = ["OfferListParams"] class OfferListParams(TypedDict, total=False): - brand: str - """Filter by brand name, case-insensitive (e.g., `Fiat`).""" + brand: Optional[str] - canone_max_eur: int - """Upper bound on displayed monthly canon (EUR).""" + canone_max_eur: Optional[int] - cursor: str - """Opaque pagination cursor.""" + cursor: Optional[str] - duration_months: Literal[24, 36, 48] + duration_months: Optional[int] - fuel_type: str - """Raw Italian label (case-insensitive ILIKE match). + fuel_type: Optional[str] - Examples: "Benzina", "Diesel", "Ibrido benzina", "Ibrido diesel", "Elettrica", - "GPL", "Metano". - """ - - km_per_year: Literal[10000, 15000, 20000, 25000, 30000, 40000] + km_per_year: Optional[int] limit: int - segment: str - """Raw Italian label (case-insensitive ILIKE substring match). - - Examples: "SUV piccoli", "SUV medi", "Superiori", "Medie", "Utilitarie". - """ + segment: Optional[str] diff --git a/src/partnermax/types/dealers/nlt/offer_list_response.py b/src/partnermax/types/dealers/nlt/offer_list_response.py index 98b291d..2ffed76 100644 --- a/src/partnermax/types/dealers/nlt/offer_list_response.py +++ b/src/partnermax/types/dealers/nlt/offer_list_response.py @@ -9,6 +9,8 @@ class OfferListResponse(BaseModel): + """Cursor-paginated list of offer summaries.""" + data: List[NltOfferSummary] has_more: bool diff --git a/src/partnermax/types/dealers/nlt/offer_retrieve_response.py b/src/partnermax/types/dealers/nlt/offer_retrieve_response.py index eae704f..08879b3 100644 --- a/src/partnermax/types/dealers/nlt/offer_retrieve_response.py +++ b/src/partnermax/types/dealers/nlt/offer_retrieve_response.py @@ -2,7 +2,6 @@ from typing import Dict, List, Optional from datetime import datetime -from typing_extensions import Literal from ...._models import BaseModel @@ -24,43 +23,79 @@ class AvailableAddonsReplacementVehicle(BaseModel): + """ + Replacement-vehicle add-on lookup (apimax: `addons_disponibili.auto_sostitutiva`). + + Always category B (utilitaria) per founder decision — the spoken + "average customer" segment. + """ + category_description: str default_category: str - """Replacement vehicle category (B fixed).""" monthly_cost_eur: float class AvailableAddonsTires(BaseModel): + """Tyre-replacement add-on lookup (apimax: `addons_disponibili.pneumatici`). + + Populated when `mnet_dettagli.pneumatici_anteriori` matches `R\\dd+` and + a row exists in `nlt_pneumatici` for that diameter. Null otherwise. + Replacement rule (founder decision 2026-05-12): 1 set of 4 tyres + every 30 000 km, rounded up. + """ + diameter_in: int - """Tyre diameter in inches.""" replacement_rule: str - """Replacement rule (e.g. one set every 30 000 km, rounded up).""" set_cost_eur: float - """Cost of one set of 4 tyres, EUR.""" class AvailableAddons(BaseModel): + """Container for optional add-ons (apimax: `addons_disponibili`).""" + replacement_vehicle: Optional[AvailableAddonsReplacementVehicle] = None + """ + Replacement-vehicle add-on lookup (apimax: + `addons_disponibili.auto_sostitutiva`). + + Always category B (utilitaria) per founder decision — the spoken "average + customer" segment. + """ tires: Optional[AvailableAddonsTires] = None + """Tyre-replacement add-on lookup (apimax: `addons_disponibili.pneumatici`). + + Populated when `mnet_dettagli.pneumatici_anteriori` matches `R\\dd+` and a row + exists in `nlt_pneumatici` for that diameter. Null otherwise. Replacement rule + (founder decision 2026-05-12): 1 set of 4 tyres every 30 000 km, rounded up. + """ class DownPaymentScenariosEur(BaseModel): + """Three down-payment scenarios in EUR (whole amounts). + + apimax: `anticipo_scenari_eur` (keys remapped to American English + snake_case for partnermax SDK: `zero/medium/standard`). + """ + medium: int - """12.5% down-payment scenario, whole EUR.""" standard: int - """25% down-payment scenario, whole EUR (matches vetrina canon).""" zero: int - """Zero down-payment scenario, whole EUR.""" class DownPaymentScenariosLabels(BaseModel): + """Italian labels paired 1:1 with `NltDownPaymentScenariosEur`. + + apimax: `anticipo_scenari_labels` — used by Custom GPT to render the + three options in conversation. Values stay in Italian raw + ("Senza anticipo" / "Anticipo 12,5%" / "Anticipo 25%"). + """ + medium: str standard: str @@ -69,23 +104,30 @@ class DownPaymentScenariosLabels(BaseModel): class Faq(BaseModel): - answer: str - """ - Italian answer (no placeholders — values from the offer payload + Motornet - specs). + """One Italian Q&A entry derived per-offer. + + apimax: `build_offer_faqs` in `seo_engine/nlt_faq_builder.py` — + generates up to ~11 Q&A pairs (dimensions, fuel, transmission, CO2, + monthly canon at preset combo, available durations, VAT inclusion, + down-payment tiers, etc.). Partnermax surfaces them all, 1:1. """ + answer: str + question: str - """Italian question.""" class Gallery(BaseModel): + """One image in the offer gallery (apimax: `gallery[]`).""" + is_cover: bool url: str class IncludedAccessory(BaseModel): + """One accessory bundled with the offer (apimax: `accessori_inclusi[]`).""" + code: str description: str @@ -94,14 +136,29 @@ class IncludedAccessory(BaseModel): class IncludedService(BaseModel): + """One NLT service normally included in the canone. + + apimax: `_get_services_included` (`nlt_resolver.py:719`). Source is + the global `nlt_services` table (active rows only). Same set of + services across the network (Assicurazione RCA / Kasco / + Incendio-Furto, Manutenzione, Assistenza Stradale, Bollo, + Pneumatici, Veicolo in anticipo, Vettura sostitutiva). Not per-offer. + """ + name: str - """Service name (e.g. "Assicurazione RCA", "Manutenzione").""" description: Optional[str] = None - """Short human description (e.g. "Responsabilità Civile Auto").""" class NetworkOffer(BaseModel): + """One network dealer's quote for this offer (apimax: `network_offers[]`). + + Sorted by `min_monthly_canon_eur ASC`. In partnermax this list is + scoped to dealers owned by the calling partner + (`utenti.parent_id = partner.user_id`) — same shape as the apimax + cross-network list, partner-scoped to avoid data leakage. + """ + dealer_id: int dealer_name: str @@ -124,32 +181,43 @@ class NetworkOffer(BaseModel): class Quotation(BaseModel): - duration_months: Literal[36, 48, 60] + """One priced cell of the 18-combination matrix. - km_per_year: Literal[10000, 15000, 20000, 25000, 30000, 40000] + apimax: `quotazioni[]` entry — `_compute_quotazioni_dealer_aware` + (mcp_server.py:180). Reflects the dealer's vetrina formula applied to + each (durata, km) combo; cells with implausible canon (<€50) are + dropped upstream, so the list may contain fewer than 18 rows. + """ - monthly_canon_eur: float - """Displayed monthly canon for this (duration, km) cell. + duration_months: int - Computed by `calcola_canone_vetrina` for the primary dealer of the partner - network; VAT-inclusive when `private_only=true`. - """ + km_per_year: int + + monthly_canon_eur: float class Tag(BaseModel): + """Category tag for an offer (apimax: `tags[]`). + + Populated from `nlt_offerta_tag` ⋈ `nlt_offerte_tag`. Examples in + production: "Promo", "Stock pronto", "GreenChoice". + """ + name: str color: Optional[str] = None - """Hex color for tag chip.""" icon: Optional[str] = None - """FontAwesome icon class.""" class OfferRetrieveResponse(BaseModel): """Full offer detail. - Field names: American English snake_case (Stripe-style SDK contract). Values: raw Italian, apimax-aligned. Shape mirrors apimax MCP `get_nlt_offer_details` (`apimax/app/api/mcp_server.py::_tool_get_nlt_offer_details`). + Shape mirrors `_tool_get_nlt_offer_details` (apimax MCP) with all + field names translated to American English snake_case for the + partner SDK contract. VALUES stay Italian raw (apimax-aligned). + The dict `technical_details` keeps Italian KEYS because they are + `mnet_dettagli` column names (raw DB). """ found: bool @@ -157,48 +225,40 @@ class OfferRetrieveResponse(BaseModel): network_dealer_count: int offer_id: str - """Numeric `nlt_offerte.id_offerta` as string. - - Same value returned by the listing endpoint. - """ slug: str - """ - Offer slug (stable identifier shared with apimax surfaces and used in canonical - URLs). - """ title: str vat_included: bool - """True when canon is VAT-inclusive (i.e. `private_only=true`).""" available_addons: Optional[AvailableAddons] = None + """Container for optional add-ons (apimax: `addons_disponibili`).""" brand: Optional[str] = None description_full: Optional[str] = None - """AI-generated long-form description.""" description_short: Optional[str] = None down_payment_scenarios_eur: Optional[DownPaymentScenariosEur] = None + """Three down-payment scenarios in EUR (whole amounts). + + apimax: `anticipo_scenari_eur` (keys remapped to American English snake_case for + partnermax SDK: `zero/medium/standard`). + """ down_payment_scenarios_labels: Optional[DownPaymentScenariosLabels] = None + """Italian labels paired 1:1 with `NltDownPaymentScenariosEur`. - faqs: Optional[List[Faq]] = None - """ - Italian FAQ for this offer — apimax `build_offer_faqs` - (`seo_engine/nlt_faq_builder.py:63`), no cap. Derived from offer payload + - Motornet specs (dimensioni, bagagliaio, CO2, motore, posti/porte, prestazioni, - canone preset 48/15k, canone minimo, durate, IVA, anticipi). + apimax: `anticipo_scenari_labels` — used by Custom GPT to render the three + options in conversation. Values stay in Italian raw ("Senza anticipo" / + "Anticipo 12,5%" / "Anticipo 25%"). """ - fuel_type: Optional[str] = None - """Raw Italian label from `nlt_offerte.alimentazione` (e.g. + faqs: Optional[List[Faq]] = None - "Benzina", "Ibrido diesel"). - """ + fuel_type: Optional[str] = None gallery: Optional[List[Gallery]] = None @@ -207,23 +267,14 @@ class OfferRetrieveResponse(BaseModel): included_accessories: Optional[List[IncludedAccessory]] = None included_services: Optional[List[IncludedService]] = None - """Services normally included in the canone. - - apimax: `_get_services_included` (`nlt_resolver.py:719`) reads global - `nlt_services` is_active table. - """ last_modified: Optional[datetime] = None min_monthly_canon_eur: Optional[float] = None - """ - Lowest canon across the partner network for this offer (primary dealer's price). - """ model: Optional[str] = None network_offers: Optional[List[NetworkOffer]] = None - """All the partner's dealers that can fulfil this offer, sorted by canon ASC.""" primary_dealer_city: Optional[str] = None @@ -232,47 +283,19 @@ class OfferRetrieveResponse(BaseModel): primary_dealer_province: Optional[str] = None private_only: Optional[bool] = None - """Per-offer VAT scope: true → consumer-facing (B2C, VAT-inclusive). - - false → business (B2B, VAT-exclusive). Sourced from `nlt_offerte.solo_privati`. - """ quotations: Optional[List[Quotation]] = None segment: Optional[str] = None - """Raw Italian label from `nlt_offerte.segmento` (e.g. - - "SUV piccoli", "Superiori"). - """ standard_equipment: Optional[List[str]] = None - """Standard equipment list (one entry per item). - - Sourced from `mnet_dettagli.equipaggiamento` split on newlines/semicolons. - Currently empty on every live offer (upstream column unpopulated); will - auto-fill when the data flows in. - """ tags: Optional[List[Tag]] = None technical_details: Optional[Dict[str, object]] = None - """ - Full Motornet technical sheet — apimax: `_get_dettagli_motornet` - (`nlt_resolver.py:752`). Every non-null `mnet_dettagli` column for this - `codice_motornet_uni` flattened into a plain dict (~30-40 keys typically - populated out of 90 columns). KEYS stay Italian because they are raw SQL column - names: cilindrata (cc), kw, hp, coppia, accelerazione (s), velocita (km/h), - lunghezza/larghezza/altezza/passo (cm), peso (kg), bagagliaio (L, free-text), - emissioni_co2 (g/km, free-text), pneumatici_anteriori ("205/55 R17"), trazione, - alimentazione, cambio, euro, autonomia_media, capacita_nominale_batteria, etc. - Native units preserved; values are int/float/bool/string (timestamps - ISO-formatted). - """ total_price_eur: Optional[float] = None - """List price IVA-inclusive (vehicle + accessories + MSS).""" transmission: Optional[str] = None - """Raw Italian label from `nlt_offerte.cambio` (e.g. "Automatico sequenziale").""" trim: Optional[str] = None diff --git a/src/partnermax/types/dealers/nlt_setting_update_params.py b/src/partnermax/types/dealers/nlt_setting_update_params.py index f58d35c..ec33f04 100644 --- a/src/partnermax/types/dealers/nlt_setting_update_params.py +++ b/src/partnermax/types/dealers/nlt_setting_update_params.py @@ -3,9 +3,8 @@ from __future__ import annotations from typing import Optional -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing_extensions import Literal, Required, TypedDict -from ..._utils import PropertyInfo from .down_payment_tiers_param import DownPaymentTiersParam __all__ = ["NltSettingUpdateParams"] @@ -17,16 +16,15 @@ class NltSettingUpdateParams(TypedDict, total=False): down_payment_tiers: Required[DownPaymentTiersParam] """Three down-payment scenarios (basso / medio / alto). - Each tier carries `{percent_of_list (0–100), fixed_eur (≥0)}`. No - strict-ascending check — the final EUR per tier is offer-dependent - (`listino_imponibile * pct + eur`). + No strict-ascending validation: the final EUR amount depends on the offer's list + price (`tier.percent_of_list / 100 * listino_imponibile + tier.fixed_eur`), so a + tier that looks larger by % can produce a smaller EUR on cheap vehicles. Label + semantics (low/medium/high) are advisory — apimax/DealerMAX UI treats the 3 + positions as opaque slots ordered by intent. """ - image_mode: Required[Literal["branded", "scenario_locked", "scenario_seasonal"]] - currency: Literal["EUR"] - image_scenario_locked: Optional[Literal["mediterraneo", "cortina", "milano", "showroom"]] - """Required when `image_mode='scenario_locked'`; must be null otherwise.""" + image_mode: Literal["branded", "scenario_locked", "scenario_seasonal"] - idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] + image_scenario_locked: Optional[Literal["mediterraneo", "cortina", "milano", "showroom"]] diff --git a/src/partnermax/types/dealers/nlt_settings.py b/src/partnermax/types/dealers/nlt_settings.py index 635500f..2f2ebc8 100644 --- a/src/partnermax/types/dealers/nlt_settings.py +++ b/src/partnermax/types/dealers/nlt_settings.py @@ -11,39 +11,31 @@ class NltSettings(BaseModel): - """Dealer-level NLT economics + image rendering preferences. + """Response model for GET / PATCH /v1/dealers/{id}/nlt-settings. - VAT treatment is NOT a dealer-level field — it is a property of the offer (see `NltOfferSummary.vat_treatment`). + Note: there is no `vat_treatment` field — VAT is a property of the + offer (`nlt_offerte.solo_privati`), not of the dealer. The offer + detail returns the VAT treatment per row instead. """ agency_markup_percent: float - """Markup applied on top of the network base canon, in percent. Hard cap at 10%.""" - - currency: Literal["EUR"] - """Only EUR supported in v1.""" dealer_id: str down_payment_tiers: DownPaymentTiers """Three down-payment scenarios (basso / medio / alto). - Each tier carries `{percent_of_list (0–100), fixed_eur (≥0)}`. No - strict-ascending check — the final EUR per tier is offer-dependent - (`listino_imponibile * pct + eur`). + No strict-ascending validation: the final EUR amount depends on the offer's list + price (`tier.percent_of_list / 100 * listino_imponibile + tier.fixed_eur`), so a + tier that looks larger by % can produce a smaller EUR on cheap vehicles. Label + semantics (low/medium/high) are advisory — apimax/DealerMAX UI treats the 3 + positions as opaque slots ordered by intent. """ effective_from: datetime - image_mode: Literal["branded", "scenario_locked", "scenario_seasonal"] - """ - How NLT offer cover images are rendered for this dealer (apimax: - `nlt_image_mode`). `branded` (default): per-dealer composite. `scenario_locked`: - single AI scenario fixed by the dealer. `scenario_seasonal`: AI scenario - auto-rotated by Italian season. - """ + currency: Optional[Literal["EUR"]] = None - image_scenario_locked: Optional[Literal["mediterraneo", "cortina", "milano", "showroom"]] = None - """Only set when `image_mode='scenario_locked'`. + image_mode: Optional[Literal["branded", "scenario_locked", "scenario_seasonal"]] = None - One of the four AI scenarios available on `mnet_modelli_ai_foto.scenario`. - """ + image_scenario_locked: Optional[Literal["mediterraneo", "cortina", "milano", "showroom"]] = None diff --git a/src/partnermax/types/key_issue_params.py b/src/partnermax/types/key_issue_params.py index f45ddac..04214a2 100644 --- a/src/partnermax/types/key_issue_params.py +++ b/src/partnermax/types/key_issue_params.py @@ -15,7 +15,7 @@ class KeyIssueParams(TypedDict, total=False): label: Required[str] """Human-readable identifier for this key, used for safe logging.""" - expires_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + expires_at: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")] """Optional expiry timestamp. Null = never expires until revoked.""" idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] diff --git a/src/partnermax/types/key_issue_response.py b/src/partnermax/types/key_issue_response.py index e6285c4..487c09a 100644 --- a/src/partnermax/types/key_issue_response.py +++ b/src/partnermax/types/key_issue_response.py @@ -1,5 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional from datetime import datetime from .._models import BaseModel @@ -8,19 +9,21 @@ class KeyIssueResponse(BaseModel): + """One-time response for ``POST /v1/keys/issue``. + + The ``key`` field is plaintext and is never retrievable again. Callers + must persist it immediately on receipt. + """ + created_at: datetime - expires_at: datetime + expires_at: Optional[datetime] = None key: str - """Plaintext key material. - - Returned ONCE — never retrievable again. Store securely. - """ + """Plaintext key material. Returned ONCE — never retrievable again.""" key_id: str key_prefix: str - """First 8 characters of the key, safe for logging.""" label: str diff --git a/src/partnermax/types/key_list_response.py b/src/partnermax/types/key_list_response.py index 75291d3..3ed2ec1 100644 --- a/src/partnermax/types/key_list_response.py +++ b/src/partnermax/types/key_list_response.py @@ -9,8 +9,12 @@ class Data(BaseModel): + """Metadata-only representation of an API key. Safe to return on list calls.""" + created_at: datetime + expires_at: Optional[datetime] = None + is_active: bool key_id: str @@ -19,10 +23,10 @@ class Data(BaseModel): label: str - expires_at: Optional[datetime] = None - last_used_at: Optional[datetime] = None class KeyListResponse(BaseModel): + """Response envelope for ``GET /v1/keys``.""" + data: List[Data] diff --git a/tests/api_resources/dealers/nlt/test_offers.py b/tests/api_resources/dealers/nlt/test_offers.py index 47df2f3..68b54a1 100644 --- a/tests/api_resources/dealers/nlt/test_offers.py +++ b/tests/api_resources/dealers/nlt/test_offers.py @@ -85,9 +85,9 @@ def test_method_list_with_all_params(self, client: Partnermax) -> None: brand="brand", canone_max_eur=50, cursor="cursor", - duration_months=24, + duration_months=0, fuel_type="fuel_type", - km_per_year=10000, + km_per_year=0, limit=1, segment="segment", ) @@ -201,9 +201,9 @@ async def test_method_list_with_all_params(self, async_client: AsyncPartnermax) brand="brand", canone_max_eur=50, cursor="cursor", - duration_months=24, + duration_months=0, fuel_type="fuel_type", - km_per_year=10000, + km_per_year=0, limit=1, segment="segment", ) diff --git a/tests/api_resources/dealers/test_nlt_settings.py b/tests/api_resources/dealers/test_nlt_settings.py index 43b0546..98bca81 100644 --- a/tests/api_resources/dealers/test_nlt_settings.py +++ b/tests/api_resources/dealers/test_nlt_settings.py @@ -64,11 +64,11 @@ def test_path_params_retrieve(self, client: Partnermax) -> None: def test_method_update(self, client: Partnermax) -> None: nlt_setting = client.dealers.nlt_settings.update( dealer_id="dealer_id", - agency_markup_percent=3.5, + agency_markup_percent=0, down_payment_tiers={ "high": { "fixed_eur": 0, - "percent_of_list": 25, + "percent_of_list": 0, }, "low": { "fixed_eur": 0, @@ -76,10 +76,9 @@ def test_method_update(self, client: Partnermax) -> None: }, "medium": { "fixed_eur": 0, - "percent_of_list": 12.5, + "percent_of_list": 0, }, }, - image_mode="branded", ) assert_matches_type(NltSettings, nlt_setting, path=["response"]) @@ -88,11 +87,11 @@ def test_method_update(self, client: Partnermax) -> None: def test_method_update_with_all_params(self, client: Partnermax) -> None: nlt_setting = client.dealers.nlt_settings.update( dealer_id="dealer_id", - agency_markup_percent=3.5, + agency_markup_percent=0, down_payment_tiers={ "high": { "fixed_eur": 0, - "percent_of_list": 25, + "percent_of_list": 0, }, "low": { "fixed_eur": 0, @@ -100,13 +99,12 @@ def test_method_update_with_all_params(self, client: Partnermax) -> None: }, "medium": { "fixed_eur": 0, - "percent_of_list": 12.5, + "percent_of_list": 0, }, }, - image_mode="branded", currency="EUR", + image_mode="branded", image_scenario_locked="mediterraneo", - idempotency_key="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(NltSettings, nlt_setting, path=["response"]) @@ -115,11 +113,11 @@ def test_method_update_with_all_params(self, client: Partnermax) -> None: def test_raw_response_update(self, client: Partnermax) -> None: response = client.dealers.nlt_settings.with_raw_response.update( dealer_id="dealer_id", - agency_markup_percent=3.5, + agency_markup_percent=0, down_payment_tiers={ "high": { "fixed_eur": 0, - "percent_of_list": 25, + "percent_of_list": 0, }, "low": { "fixed_eur": 0, @@ -127,10 +125,9 @@ def test_raw_response_update(self, client: Partnermax) -> None: }, "medium": { "fixed_eur": 0, - "percent_of_list": 12.5, + "percent_of_list": 0, }, }, - image_mode="branded", ) assert response.is_closed is True @@ -143,11 +140,11 @@ def test_raw_response_update(self, client: Partnermax) -> None: def test_streaming_response_update(self, client: Partnermax) -> None: with client.dealers.nlt_settings.with_streaming_response.update( dealer_id="dealer_id", - agency_markup_percent=3.5, + agency_markup_percent=0, down_payment_tiers={ "high": { "fixed_eur": 0, - "percent_of_list": 25, + "percent_of_list": 0, }, "low": { "fixed_eur": 0, @@ -155,10 +152,9 @@ def test_streaming_response_update(self, client: Partnermax) -> None: }, "medium": { "fixed_eur": 0, - "percent_of_list": 12.5, + "percent_of_list": 0, }, }, - image_mode="branded", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -174,11 +170,11 @@ def test_path_params_update(self, client: Partnermax) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): client.dealers.nlt_settings.with_raw_response.update( dealer_id="", - agency_markup_percent=3.5, + agency_markup_percent=0, down_payment_tiers={ "high": { "fixed_eur": 0, - "percent_of_list": 25, + "percent_of_list": 0, }, "low": { "fixed_eur": 0, @@ -186,10 +182,9 @@ def test_path_params_update(self, client: Partnermax) -> None: }, "medium": { "fixed_eur": 0, - "percent_of_list": 12.5, + "percent_of_list": 0, }, }, - image_mode="branded", ) @@ -245,11 +240,11 @@ async def test_path_params_retrieve(self, async_client: AsyncPartnermax) -> None async def test_method_update(self, async_client: AsyncPartnermax) -> None: nlt_setting = await async_client.dealers.nlt_settings.update( dealer_id="dealer_id", - agency_markup_percent=3.5, + agency_markup_percent=0, down_payment_tiers={ "high": { "fixed_eur": 0, - "percent_of_list": 25, + "percent_of_list": 0, }, "low": { "fixed_eur": 0, @@ -257,10 +252,9 @@ async def test_method_update(self, async_client: AsyncPartnermax) -> None: }, "medium": { "fixed_eur": 0, - "percent_of_list": 12.5, + "percent_of_list": 0, }, }, - image_mode="branded", ) assert_matches_type(NltSettings, nlt_setting, path=["response"]) @@ -269,11 +263,11 @@ async def test_method_update(self, async_client: AsyncPartnermax) -> None: async def test_method_update_with_all_params(self, async_client: AsyncPartnermax) -> None: nlt_setting = await async_client.dealers.nlt_settings.update( dealer_id="dealer_id", - agency_markup_percent=3.5, + agency_markup_percent=0, down_payment_tiers={ "high": { "fixed_eur": 0, - "percent_of_list": 25, + "percent_of_list": 0, }, "low": { "fixed_eur": 0, @@ -281,13 +275,12 @@ async def test_method_update_with_all_params(self, async_client: AsyncPartnermax }, "medium": { "fixed_eur": 0, - "percent_of_list": 12.5, + "percent_of_list": 0, }, }, - image_mode="branded", currency="EUR", + image_mode="branded", image_scenario_locked="mediterraneo", - idempotency_key="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(NltSettings, nlt_setting, path=["response"]) @@ -296,11 +289,11 @@ async def test_method_update_with_all_params(self, async_client: AsyncPartnermax async def test_raw_response_update(self, async_client: AsyncPartnermax) -> None: response = await async_client.dealers.nlt_settings.with_raw_response.update( dealer_id="dealer_id", - agency_markup_percent=3.5, + agency_markup_percent=0, down_payment_tiers={ "high": { "fixed_eur": 0, - "percent_of_list": 25, + "percent_of_list": 0, }, "low": { "fixed_eur": 0, @@ -308,10 +301,9 @@ async def test_raw_response_update(self, async_client: AsyncPartnermax) -> None: }, "medium": { "fixed_eur": 0, - "percent_of_list": 12.5, + "percent_of_list": 0, }, }, - image_mode="branded", ) assert response.is_closed is True @@ -324,11 +316,11 @@ async def test_raw_response_update(self, async_client: AsyncPartnermax) -> None: async def test_streaming_response_update(self, async_client: AsyncPartnermax) -> None: async with async_client.dealers.nlt_settings.with_streaming_response.update( dealer_id="dealer_id", - agency_markup_percent=3.5, + agency_markup_percent=0, down_payment_tiers={ "high": { "fixed_eur": 0, - "percent_of_list": 25, + "percent_of_list": 0, }, "low": { "fixed_eur": 0, @@ -336,10 +328,9 @@ async def test_streaming_response_update(self, async_client: AsyncPartnermax) -> }, "medium": { "fixed_eur": 0, - "percent_of_list": 12.5, + "percent_of_list": 0, }, }, - image_mode="branded", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -355,11 +346,11 @@ async def test_path_params_update(self, async_client: AsyncPartnermax) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): await async_client.dealers.nlt_settings.with_raw_response.update( dealer_id="", - agency_markup_percent=3.5, + agency_markup_percent=0, down_payment_tiers={ "high": { "fixed_eur": 0, - "percent_of_list": 25, + "percent_of_list": 0, }, "low": { "fixed_eur": 0, @@ -367,8 +358,7 @@ async def test_path_params_update(self, async_client: AsyncPartnermax) -> None: }, "medium": { "fixed_eur": 0, - "percent_of_list": 12.5, + "percent_of_list": 0, }, }, - image_mode="branded", ) diff --git a/tests/api_resources/test_dealers.py b/tests/api_resources/test_dealers.py index 2fb2532..dd5d3fa 100644 --- a/tests/api_resources/test_dealers.py +++ b/tests/api_resources/test_dealers.py @@ -24,12 +24,15 @@ class TestDealers: @parametrize def test_method_create(self, client: Partnermax) -> None: dealer = client.dealers.create( - business_name="Rossi Automobili S.R.L.", - contact_email="info@rossi-auto.it", - postal_code="20121", - primary_domain="rossi-auto.it", - province_code="MI", - vat_number="IT01234567890", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + postal_code="21029", + primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", + province_code="SE", + vat_number="IT21029798095", ) assert_matches_type(DealerDetail, dealer, path=["response"]) @@ -37,16 +40,18 @@ def test_method_create(self, client: Partnermax) -> None: @parametrize def test_method_create_with_all_params(self, client: Partnermax) -> None: dealer = client.dealers.create( - business_name="Rossi Automobili S.R.L.", - contact_email="info@rossi-auto.it", - postal_code="20121", - primary_domain="rossi-auto.it", - province_code="MI", - vat_number="IT01234567890", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + postal_code="21029", + primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", + province_code="SE", + vat_number="IT21029798095", activate=True, - contact_phone="+390212345678", - metadata={"partner_internal_id": "DLR-9182"}, - idempotency_key="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + metadata={"foo": "string"}, + idempotency_key="Idempotency-Key", ) assert_matches_type(DealerDetail, dealer, path=["response"]) @@ -54,12 +59,15 @@ def test_method_create_with_all_params(self, client: Partnermax) -> None: @parametrize def test_raw_response_create(self, client: Partnermax) -> None: response = client.dealers.with_raw_response.create( - business_name="Rossi Automobili S.R.L.", - contact_email="info@rossi-auto.it", - postal_code="20121", - primary_domain="rossi-auto.it", - province_code="MI", - vat_number="IT01234567890", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + postal_code="21029", + primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", + province_code="SE", + vat_number="IT21029798095", ) assert response.is_closed is True @@ -71,12 +79,15 @@ def test_raw_response_create(self, client: Partnermax) -> None: @parametrize def test_streaming_response_create(self, client: Partnermax) -> None: with client.dealers.with_streaming_response.create( - business_name="Rossi Automobili S.R.L.", - contact_email="info@rossi-auto.it", - postal_code="20121", - primary_domain="rossi-auto.it", - province_code="MI", - vat_number="IT01234567890", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + postal_code="21029", + primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", + province_code="SE", + vat_number="IT21029798095", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -141,14 +152,16 @@ def test_method_update(self, client: Partnermax) -> None: def test_method_update_with_all_params(self, client: Partnermax) -> None: dealer = client.dealers.update( dealer_id="dealer_id", - business_name="business_name", + address="xx", + business_name="xx", + city="xx", contact_email="dev@stainless.com", - contact_phone="contact_phone", + contact_phone="xxxxx", metadata={"foo": "string"}, postal_code="21029", province_code="SE", status="active", - idempotency_key="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + idempotency_key="Idempotency-Key", ) assert_matches_type(DealerDetail, dealer, path=["response"]) @@ -276,12 +289,15 @@ class TestAsyncDealers: @parametrize async def test_method_create(self, async_client: AsyncPartnermax) -> None: dealer = await async_client.dealers.create( - business_name="Rossi Automobili S.R.L.", - contact_email="info@rossi-auto.it", - postal_code="20121", - primary_domain="rossi-auto.it", - province_code="MI", - vat_number="IT01234567890", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + postal_code="21029", + primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", + province_code="SE", + vat_number="IT21029798095", ) assert_matches_type(DealerDetail, dealer, path=["response"]) @@ -289,16 +305,18 @@ async def test_method_create(self, async_client: AsyncPartnermax) -> None: @parametrize async def test_method_create_with_all_params(self, async_client: AsyncPartnermax) -> None: dealer = await async_client.dealers.create( - business_name="Rossi Automobili S.R.L.", - contact_email="info@rossi-auto.it", - postal_code="20121", - primary_domain="rossi-auto.it", - province_code="MI", - vat_number="IT01234567890", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + postal_code="21029", + primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", + province_code="SE", + vat_number="IT21029798095", activate=True, - contact_phone="+390212345678", - metadata={"partner_internal_id": "DLR-9182"}, - idempotency_key="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + metadata={"foo": "string"}, + idempotency_key="Idempotency-Key", ) assert_matches_type(DealerDetail, dealer, path=["response"]) @@ -306,12 +324,15 @@ async def test_method_create_with_all_params(self, async_client: AsyncPartnermax @parametrize async def test_raw_response_create(self, async_client: AsyncPartnermax) -> None: response = await async_client.dealers.with_raw_response.create( - business_name="Rossi Automobili S.R.L.", - contact_email="info@rossi-auto.it", - postal_code="20121", - primary_domain="rossi-auto.it", - province_code="MI", - vat_number="IT01234567890", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + postal_code="21029", + primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", + province_code="SE", + vat_number="IT21029798095", ) assert response.is_closed is True @@ -323,12 +344,15 @@ async def test_raw_response_create(self, async_client: AsyncPartnermax) -> None: @parametrize async def test_streaming_response_create(self, async_client: AsyncPartnermax) -> None: async with async_client.dealers.with_streaming_response.create( - business_name="Rossi Automobili S.R.L.", - contact_email="info@rossi-auto.it", - postal_code="20121", - primary_domain="rossi-auto.it", - province_code="MI", - vat_number="IT01234567890", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + postal_code="21029", + primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", + province_code="SE", + vat_number="IT21029798095", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -393,14 +417,16 @@ async def test_method_update(self, async_client: AsyncPartnermax) -> None: async def test_method_update_with_all_params(self, async_client: AsyncPartnermax) -> None: dealer = await async_client.dealers.update( dealer_id="dealer_id", - business_name="business_name", + address="xx", + business_name="xx", + city="xx", contact_email="dev@stainless.com", - contact_phone="contact_phone", + contact_phone="xxxxx", metadata={"foo": "string"}, postal_code="21029", province_code="SE", status="active", - idempotency_key="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + idempotency_key="Idempotency-Key", ) assert_matches_type(DealerDetail, dealer, path=["response"]) diff --git a/tests/api_resources/test_keys.py b/tests/api_resources/test_keys.py index 96f2897..482b3f1 100644 --- a/tests/api_resources/test_keys.py +++ b/tests/api_resources/test_keys.py @@ -50,7 +50,7 @@ def test_streaming_response_list(self, client: Partnermax) -> None: @parametrize def test_method_issue(self, client: Partnermax) -> None: key = client.keys.issue( - label="production-backend-2026", + label="x", ) assert_matches_type(KeyIssueResponse, key, path=["response"]) @@ -58,9 +58,9 @@ def test_method_issue(self, client: Partnermax) -> None: @parametrize def test_method_issue_with_all_params(self, client: Partnermax) -> None: key = client.keys.issue( - label="production-backend-2026", + label="x", expires_at=parse_datetime("2019-12-27T18:11:19.117Z"), - idempotency_key="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + idempotency_key="Idempotency-Key", ) assert_matches_type(KeyIssueResponse, key, path=["response"]) @@ -68,7 +68,7 @@ def test_method_issue_with_all_params(self, client: Partnermax) -> None: @parametrize def test_raw_response_issue(self, client: Partnermax) -> None: response = client.keys.with_raw_response.issue( - label="production-backend-2026", + label="x", ) assert response.is_closed is True @@ -80,7 +80,7 @@ def test_raw_response_issue(self, client: Partnermax) -> None: @parametrize def test_streaming_response_issue(self, client: Partnermax) -> None: with client.keys.with_streaming_response.issue( - label="production-backend-2026", + label="x", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -170,7 +170,7 @@ async def test_streaming_response_list(self, async_client: AsyncPartnermax) -> N @parametrize async def test_method_issue(self, async_client: AsyncPartnermax) -> None: key = await async_client.keys.issue( - label="production-backend-2026", + label="x", ) assert_matches_type(KeyIssueResponse, key, path=["response"]) @@ -178,9 +178,9 @@ async def test_method_issue(self, async_client: AsyncPartnermax) -> None: @parametrize async def test_method_issue_with_all_params(self, async_client: AsyncPartnermax) -> None: key = await async_client.keys.issue( - label="production-backend-2026", + label="x", expires_at=parse_datetime("2019-12-27T18:11:19.117Z"), - idempotency_key="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + idempotency_key="Idempotency-Key", ) assert_matches_type(KeyIssueResponse, key, path=["response"]) @@ -188,7 +188,7 @@ async def test_method_issue_with_all_params(self, async_client: AsyncPartnermax) @parametrize async def test_raw_response_issue(self, async_client: AsyncPartnermax) -> None: response = await async_client.keys.with_raw_response.issue( - label="production-backend-2026", + label="x", ) assert response.is_closed is True @@ -200,7 +200,7 @@ async def test_raw_response_issue(self, async_client: AsyncPartnermax) -> None: @parametrize async def test_streaming_response_issue(self, async_client: AsyncPartnermax) -> None: async with async_client.keys.with_streaming_response.issue( - label="production-backend-2026", + label="x", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From c817349ace294552a56e191f975a444a92b82548 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:18:18 +0000 Subject: [PATCH 2/3] feat(vehicles): used-vehicle CRUD + bulk insert for v1.1.0 --- .stats.yml | 6 +- api.md | 23 + src/partnermax/resources/dealers/__init__.py | 14 + src/partnermax/resources/dealers/dealers.py | 56 + src/partnermax/resources/dealers/vehicles.py | 1080 +++++++++++++++++ src/partnermax/types/dealers/__init__.py | 10 + .../dealers/bulk_create_vehicles_response.py | 27 + .../types/dealers/bulk_row_outcome.py | 47 + .../types/dealers/vehicle_bulk_params.py | 130 ++ .../types/dealers/vehicle_create_params.py | 107 ++ .../types/dealers/vehicle_detail.py | 100 ++ src/partnermax/types/dealers/vehicle_list.py | 18 + .../types/dealers/vehicle_list_params.py | 26 + .../types/dealers/vehicle_retrieve_params.py | 18 + .../types/dealers/vehicle_summary.py | 47 + .../types/dealers/vehicle_update_params.py | 59 + tests/api_resources/dealers/test_vehicles.py | 973 +++++++++++++++ 17 files changed, 2738 insertions(+), 3 deletions(-) create mode 100644 src/partnermax/resources/dealers/vehicles.py create mode 100644 src/partnermax/types/dealers/bulk_create_vehicles_response.py create mode 100644 src/partnermax/types/dealers/bulk_row_outcome.py create mode 100644 src/partnermax/types/dealers/vehicle_bulk_params.py create mode 100644 src/partnermax/types/dealers/vehicle_create_params.py create mode 100644 src/partnermax/types/dealers/vehicle_detail.py create mode 100644 src/partnermax/types/dealers/vehicle_list.py create mode 100644 src/partnermax/types/dealers/vehicle_list_params.py create mode 100644 src/partnermax/types/dealers/vehicle_retrieve_params.py create mode 100644 src/partnermax/types/dealers/vehicle_summary.py create mode 100644 src/partnermax/types/dealers/vehicle_update_params.py create mode 100644 tests/api_resources/dealers/test_vehicles.py diff --git a/.stats.yml b/.stats.yml index a686673..26bad1a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 12 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-1f6135961fb19ff430c509fdffab0b2441ce875d3da69971921e4d231f57b25c.yml +configured_endpoints: 18 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-2efc95334578123c6faee89c66575c440aec573d479dd6b6e0ce0d6477472e23.yml openapi_spec_hash: c97c1474daf815592ca1a3a9fd29e482 -config_hash: bf921bb2f4db038ffed15141c74cd018 +config_hash: ef989e9f9b2271740f1b742f1ab804af diff --git a/api.md b/api.md index 860568b..bca3acb 100644 --- a/api.md +++ b/api.md @@ -55,3 +55,26 @@ Methods: - client.dealers.nlt.offers.retrieve(offer_id, \*, dealer_id) -> OfferRetrieveResponse - client.dealers.nlt.offers.list(dealer_id, \*\*params) -> OfferListResponse + +## Vehicles + +Types: + +```python +from partnermax.types.dealers import ( + BulkCreateVehiclesResponse, + BulkRowOutcome, + VehicleDetail, + VehicleList, + VehicleSummary, +) +``` + +Methods: + +- client.dealers.vehicles.create(dealer_id, \*\*params) -> VehicleDetail +- client.dealers.vehicles.retrieve(vehicle_id, \*, dealer_id, \*\*params) -> VehicleDetail +- client.dealers.vehicles.update(vehicle_id, \*, dealer_id, \*\*params) -> VehicleDetail +- client.dealers.vehicles.list(dealer_id, \*\*params) -> VehicleList +- client.dealers.vehicles.delete(vehicle_id, \*, dealer_id) -> None +- client.dealers.vehicles.bulk(dealer_id, \*\*params) -> BulkCreateVehiclesResponse diff --git a/src/partnermax/resources/dealers/__init__.py b/src/partnermax/resources/dealers/__init__.py index 2537f24..edd3e7c 100644 --- a/src/partnermax/resources/dealers/__init__.py +++ b/src/partnermax/resources/dealers/__init__.py @@ -16,6 +16,14 @@ DealersResourceWithStreamingResponse, AsyncDealersResourceWithStreamingResponse, ) +from .vehicles import ( + VehiclesResource, + AsyncVehiclesResource, + VehiclesResourceWithRawResponse, + AsyncVehiclesResourceWithRawResponse, + VehiclesResourceWithStreamingResponse, + AsyncVehiclesResourceWithStreamingResponse, +) from .nlt_settings import ( NltSettingsResource, AsyncNltSettingsResource, @@ -38,6 +46,12 @@ "AsyncNltResourceWithRawResponse", "NltResourceWithStreamingResponse", "AsyncNltResourceWithStreamingResponse", + "VehiclesResource", + "AsyncVehiclesResource", + "VehiclesResourceWithRawResponse", + "AsyncVehiclesResourceWithRawResponse", + "VehiclesResourceWithStreamingResponse", + "AsyncVehiclesResourceWithStreamingResponse", "DealersResource", "AsyncDealersResource", "DealersResourceWithRawResponse", diff --git a/src/partnermax/resources/dealers/dealers.py b/src/partnermax/resources/dealers/dealers.py index 1dadb84..bb4b7da 100644 --- a/src/partnermax/resources/dealers/dealers.py +++ b/src/partnermax/resources/dealers/dealers.py @@ -18,6 +18,14 @@ ) from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform +from .vehicles import ( + VehiclesResource, + AsyncVehiclesResource, + VehiclesResourceWithRawResponse, + AsyncVehiclesResourceWithRawResponse, + VehiclesResourceWithStreamingResponse, + AsyncVehiclesResourceWithStreamingResponse, +) from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -52,6 +60,14 @@ def nlt_settings(self) -> NltSettingsResource: def nlt(self) -> NltResource: return NltResource(self._client) + @cached_property + def vehicles(self) -> VehiclesResource: + """Used-vehicle stock management for partner-owned dealers. + + The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer). + """ + return VehiclesResource(self._client) + @cached_property def with_raw_response(self) -> DealersResourceWithRawResponse: """ @@ -315,6 +331,14 @@ def nlt_settings(self) -> AsyncNltSettingsResource: def nlt(self) -> AsyncNltResource: return AsyncNltResource(self._client) + @cached_property + def vehicles(self) -> AsyncVehiclesResource: + """Used-vehicle stock management for partner-owned dealers. + + The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer). + """ + return AsyncVehiclesResource(self._client) + @cached_property def with_raw_response(self) -> AsyncDealersResourceWithRawResponse: """ @@ -595,6 +619,14 @@ def nlt_settings(self) -> NltSettingsResourceWithRawResponse: def nlt(self) -> NltResourceWithRawResponse: return NltResourceWithRawResponse(self._dealers.nlt) + @cached_property + def vehicles(self) -> VehiclesResourceWithRawResponse: + """Used-vehicle stock management for partner-owned dealers. + + The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer). + """ + return VehiclesResourceWithRawResponse(self._dealers.vehicles) + class AsyncDealersResourceWithRawResponse: def __init__(self, dealers: AsyncDealersResource) -> None: @@ -624,6 +656,14 @@ def nlt_settings(self) -> AsyncNltSettingsResourceWithRawResponse: def nlt(self) -> AsyncNltResourceWithRawResponse: return AsyncNltResourceWithRawResponse(self._dealers.nlt) + @cached_property + def vehicles(self) -> AsyncVehiclesResourceWithRawResponse: + """Used-vehicle stock management for partner-owned dealers. + + The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer). + """ + return AsyncVehiclesResourceWithRawResponse(self._dealers.vehicles) + class DealersResourceWithStreamingResponse: def __init__(self, dealers: DealersResource) -> None: @@ -653,6 +693,14 @@ def nlt_settings(self) -> NltSettingsResourceWithStreamingResponse: def nlt(self) -> NltResourceWithStreamingResponse: return NltResourceWithStreamingResponse(self._dealers.nlt) + @cached_property + def vehicles(self) -> VehiclesResourceWithStreamingResponse: + """Used-vehicle stock management for partner-owned dealers. + + The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer). + """ + return VehiclesResourceWithStreamingResponse(self._dealers.vehicles) + class AsyncDealersResourceWithStreamingResponse: def __init__(self, dealers: AsyncDealersResource) -> None: @@ -681,3 +729,11 @@ def nlt_settings(self) -> AsyncNltSettingsResourceWithStreamingResponse: @cached_property def nlt(self) -> AsyncNltResourceWithStreamingResponse: return AsyncNltResourceWithStreamingResponse(self._dealers.nlt) + + @cached_property + def vehicles(self) -> AsyncVehiclesResourceWithStreamingResponse: + """Used-vehicle stock management for partner-owned dealers. + + The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer). + """ + return AsyncVehiclesResourceWithStreamingResponse(self._dealers.vehicles) diff --git a/src/partnermax/resources/dealers/vehicles.py b/src/partnermax/resources/dealers/vehicles.py new file mode 100644 index 0000000..99332b1 --- /dev/null +++ b/src/partnermax/resources/dealers/vehicles.py @@ -0,0 +1,1080 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable, Optional +from datetime import date + +import httpx + +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.dealers import ( + vehicle_bulk_params, + vehicle_list_params, + vehicle_create_params, + vehicle_update_params, + vehicle_retrieve_params, +) +from ...types.dealers.vehicle_list import VehicleList +from ...types.dealers.vehicle_detail import VehicleDetail +from ...types.dealers.bulk_create_vehicles_response import BulkCreateVehiclesResponse + +__all__ = ["VehiclesResource", "AsyncVehiclesResource"] + + +class VehiclesResource(SyncAPIResource): + """Used-vehicle stock management for partner-owned dealers. + + The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer). + """ + + @cached_property + def with_raw_response(self) -> VehiclesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/DealerMax-app/partnermax-python#accessing-raw-response-data-eg-headers + """ + return VehiclesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> VehiclesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/DealerMax-app/partnermax-python#with_streaming_response + """ + return VehiclesResourceWithStreamingResponse(self) + + def create( + self, + dealer_id: str, + *, + certified_km: int, + cost_price_eur: float, + motornet_code: str, + plate: str, + registration_year: int, + sale_price_eur: float, + alloy_wheel_size: Optional[int] | Omit = omit, + color: Optional[str] | Omit = omit, + description: str | Omit = omit, + extended_warranty_enabled: bool | Omit = omit, + extended_warranty_months: Optional[int] | Omit = omit, + inspection_expiry_date: Union[str, date, None] | Omit = omit, + is_for_sale: bool | Omit = omit, + is_visible: bool | Omit = omit, + last_service_date: Union[str, date, None] | Omit = omit, + last_service_km: Optional[int] | Omit = omit, + last_service_notes: Optional[str] | Omit = omit, + notes: Optional[str] | Omit = omit, + previous_owner_count: Optional[int] | Omit = omit, + previous_ownership_transfer_date: Union[str, date, None] | Omit = omit, + registration_month: Optional[int] | Omit = omit, + road_tax_expiry_date: Union[str, date, None] | Omit = omit, + vat_displayed: bool | Omit = omit, + vehicle_damaged: bool | Omit = omit, + vin: Optional[str] | Omit = omit, + idempotency_key: str | 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, + ) -> VehicleDetail: + """ + Provision a new used vehicle in a dealer's stock. + + Writes are atomic across `azlease_usatoin` and `azlease_usatoauto` using a + `SAVEPOINT` so a UNIQUE plate violation rolls back cleanly. On success the + AI-content worker (:mod:`azurenet-engine.app.jobs.usato_ai_content_worker`) + picks up the new row within 60 seconds and generates the SEO body + pgvector + embedding — at which point the vehicle becomes discoverable on the cross-network + MCP / Custom GPT / NLWeb surfaces. The response returns immediately (no + synchronous wait on the worker). + + Args: + certified_km: Certified odometer reading at intake, in kilometres. + + cost_price_eur: Cost basis to the dealer in EUR (partner/dealer internal). Not surfaced on + consumer-facing AI surfaces; used by dealer reporting and margin analytics only. + + motornet_code: Motornet UNI code identifying the exact vehicle configuration. Must exist in + `mnet_dettagli_usato` at submission time; otherwise the call returns 422 + `motornet_code_not_in_catalogue`. The partner is expected to source this from + its own DMS; partnermax does not expose a plate→code lookup. + + plate: Italian licence plate. Uppercased server-side. UNIQUE across the network for + active vehicles (`visibile=true AND venduto_il IS NULL`); reusable once the + previous holder sells/hides the row. + + registration_year: Year of first registration. Upper bound is current year + 1. + + sale_price_eur: Public sale price in EUR. Surfaced on MCP / Custom GPT / NLWeb and on the + dealer's site JSON-LD `Offer.price`. + + description: Partner-supplied long description. Surfaced on the dealer site detail page. + + is_for_sale: Maps to `azlease_usatoauto.is_vendita_enabled`. When false the row is in stock + but not offered for sale. + + is_visible: Soft-publish flag. When false the row exists in stock but is excluded from + consumer-facing AI surfaces. Maps to `azlease_usatoin.visibile`. + + notes: Free-form short notes; surfaced as `mnet_dettagli.precisazioni`-style. + + previous_ownership_transfer_date: Date of the most recent ownership transfer, if known. + + registration_month: Month of registration (1–12). + + vat_displayed: If true the public price is displayed VAT-exposed (B2B); otherwise VAT-inclusive + (B2C). + + vin: ISO 3779 vehicle identification number. Optional but strongly recommended. + + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return self._post( + path_template("/v1/dealers/{dealer_id}/vehicles", dealer_id=dealer_id), + body=maybe_transform( + { + "certified_km": certified_km, + "cost_price_eur": cost_price_eur, + "motornet_code": motornet_code, + "plate": plate, + "registration_year": registration_year, + "sale_price_eur": sale_price_eur, + "alloy_wheel_size": alloy_wheel_size, + "color": color, + "description": description, + "extended_warranty_enabled": extended_warranty_enabled, + "extended_warranty_months": extended_warranty_months, + "inspection_expiry_date": inspection_expiry_date, + "is_for_sale": is_for_sale, + "is_visible": is_visible, + "last_service_date": last_service_date, + "last_service_km": last_service_km, + "last_service_notes": last_service_notes, + "notes": notes, + "previous_owner_count": previous_owner_count, + "previous_ownership_transfer_date": previous_ownership_transfer_date, + "registration_month": registration_month, + "road_tax_expiry_date": road_tax_expiry_date, + "vat_displayed": vat_displayed, + "vehicle_damaged": vehicle_damaged, + "vin": vin, + }, + vehicle_create_params.VehicleCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=VehicleDetail, + ) + + def retrieve( + self, + vehicle_id: str, + *, + dealer_id: str, + include_deleted: bool | 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, + ) -> VehicleDetail: + """ + Get Vehicle + + Args: + include_deleted: If true, the detail of a soft-deleted vehicle is returned. Default false — + soft-deleted rows return 404 to keep behaviour consistent with the list + endpoint. + + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + if not vehicle_id: + raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") + return self._get( + path_template("/v1/dealers/{dealer_id}/vehicles/{vehicle_id}", dealer_id=dealer_id, vehicle_id=vehicle_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + {"include_deleted": include_deleted}, vehicle_retrieve_params.VehicleRetrieveParams + ), + ), + cast_to=VehicleDetail, + ) + + def update( + self, + vehicle_id: str, + *, + dealer_id: str, + alloy_wheel_size: Optional[int] | Omit = omit, + certified_km: Optional[int] | Omit = omit, + color: Optional[str] | Omit = omit, + cost_price_eur: Optional[float] | Omit = omit, + description: Optional[str] | Omit = omit, + extended_warranty_enabled: Optional[bool] | Omit = omit, + extended_warranty_months: Optional[int] | Omit = omit, + inspection_expiry_date: Union[str, date, None] | Omit = omit, + is_for_sale: Optional[bool] | Omit = omit, + is_visible: Optional[bool] | Omit = omit, + last_service_date: Union[str, date, None] | Omit = omit, + last_service_km: Optional[int] | Omit = omit, + last_service_notes: Optional[str] | Omit = omit, + notes: Optional[str] | Omit = omit, + previous_owner_count: Optional[int] | Omit = omit, + previous_ownership_transfer_date: Union[str, date, None] | Omit = omit, + registration_month: Optional[int] | Omit = omit, + road_tax_expiry_date: Union[str, date, None] | Omit = omit, + sale_price_eur: Optional[float] | Omit = omit, + vat_displayed: Optional[bool] | Omit = omit, + vehicle_damaged: Optional[bool] | Omit = omit, + idempotency_key: str | 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, + ) -> VehicleDetail: + """ + Partial update of a vehicle. + + Splits the inbound body across the two physical tables (`azlease_usatoauto` and + `azlease_usatoin`) and emits at most one UPDATE per table inside a single + transaction. Fields not present in the body are not touched. + + Args: + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + if not vehicle_id: + raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") + extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return self._patch( + path_template("/v1/dealers/{dealer_id}/vehicles/{vehicle_id}", dealer_id=dealer_id, vehicle_id=vehicle_id), + body=maybe_transform( + { + "alloy_wheel_size": alloy_wheel_size, + "certified_km": certified_km, + "color": color, + "cost_price_eur": cost_price_eur, + "description": description, + "extended_warranty_enabled": extended_warranty_enabled, + "extended_warranty_months": extended_warranty_months, + "inspection_expiry_date": inspection_expiry_date, + "is_for_sale": is_for_sale, + "is_visible": is_visible, + "last_service_date": last_service_date, + "last_service_km": last_service_km, + "last_service_notes": last_service_notes, + "notes": notes, + "previous_owner_count": previous_owner_count, + "previous_ownership_transfer_date": previous_ownership_transfer_date, + "registration_month": registration_month, + "road_tax_expiry_date": road_tax_expiry_date, + "sale_price_eur": sale_price_eur, + "vat_displayed": vat_displayed, + "vehicle_damaged": vehicle_damaged, + }, + vehicle_update_params.VehicleUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=VehicleDetail, + ) + + def list( + self, + dealer_id: str, + *, + cursor: Optional[str] | Omit = omit, + include_deleted: bool | Omit = omit, + is_for_sale: Optional[bool] | Omit = omit, + is_visible: Optional[bool] | Omit = omit, + limit: int | 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, + ) -> VehicleList: + """ + List vehicles in a dealer's stock owned by the calling partner. + + Cursor pagination is opaque base64url over the last vehicle UUID. Default sort + is `i.data_inserimento ASC` so freshly provisioned vehicles surface at the tail. + Soft-deleted rows are excluded unless `include_deleted=true` is set explicitly — + this preserves the soft-delete semantic across the API contract. + + Args: + include_deleted: If true, soft-deleted rows (`venduto_il` populated) are also returned. Default + false — listings hide soft-deleted vehicles. + + is_for_sale: Filter on the sale flag. + + is_visible: Filter on the visibility flag. + + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + return self._get( + path_template("/v1/dealers/{dealer_id}/vehicles", dealer_id=dealer_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "cursor": cursor, + "include_deleted": include_deleted, + "is_for_sale": is_for_sale, + "is_visible": is_visible, + "limit": limit, + }, + vehicle_list_params.VehicleListParams, + ), + ), + cast_to=VehicleList, + ) + + def delete( + self, + vehicle_id: str, + *, + dealer_id: str, + # 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, + ) -> None: + """ + Withdraw a vehicle from sale without deleting the row. + + Sets `azlease_usatoin.visibile = FALSE` and stamps `venduto_il = now()`. The + plate becomes reusable on the network the moment this returns (the + active-uniqueness check excludes rows where `visibile = FALSE` OR + `venduto_il IS NOT NULL`). + + Soft-delete is the canonical "remove this vehicle from sale" surface. The + AI-citation consumers (MCP `_tool_search_vehicles`, Custom GPT + `search_vehicles_network`, NLWeb `/ask`) each filter their own queries on + `i.visibile = TRUE AND i.venduto_il IS NULL` — the shared `v_apimax_listing` + view itself does not impose that filter, every consumer adds it. The result on + the partner side is the same: a soft-deleted vehicle disappears from every AI + surface within the next index cycle. + + Returns `409 vehicle_already_deleted` if the row is already soft- deleted — same + idempotency pattern as the dealers DELETE endpoint. + + Args: + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + if not vehicle_id: + raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template("/v1/dealers/{dealer_id}/vehicles/{vehicle_id}", dealer_id=dealer_id, vehicle_id=vehicle_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def bulk( + self, + dealer_id: str, + *, + vehicles: Iterable[vehicle_bulk_params.Vehicle], + idempotency_key: str | 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, + ) -> BulkCreateVehiclesResponse: + """ + Provision up to `BULK_MAX_ROWS` vehicles in a single synchronous call. + + Each row is processed inside its own `SAVEPOINT` so a failure on row N + (validation, plate conflict, motornet not in catalogue, race) is isolated — the + SAVEPOINT rolls back, the per-row outcome is collected with a structured error + code, and the loop continues with row N+1. + + Successful rows accumulate in the outer transaction and are committed together + at the end of the request. Failed rows leave no trace in the database. + + Returns `207 Multi-Status`. The response carries: + + - `total`, `succeeded`, `failed` — aggregate counters for quick branch logic on + the partner side. + - `results` — array of per-row outcomes, indexed by the position in the request + `vehicles[]` array. Successful rows include the full `VehicleDetail`; failed + rows include `error_code` + `error_message` keyed to the same codes as the + single-POST surface so the partner reuses one error handler for both paths. + + For imports larger than `BULK_MAX_ROWS` (currently 100), the partner is expected + to chunk the array client-side. A 5 000-vehicle initial migration is 50 calls; + the partner controls concurrency. + + Args: + vehicles: Array of vehicles to create. Between 1 and 100 rows per call. For larger + imports, the partner is expected to chunk client-side (e.g. 50 calls of 100 rows + each for a 5 000-vehicle migration). + + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return self._post( + path_template("/v1/dealers/{dealer_id}/vehicles/bulk", dealer_id=dealer_id), + body=maybe_transform({"vehicles": vehicles}, vehicle_bulk_params.VehicleBulkParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BulkCreateVehiclesResponse, + ) + + +class AsyncVehiclesResource(AsyncAPIResource): + """Used-vehicle stock management for partner-owned dealers. + + The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer). + """ + + @cached_property + def with_raw_response(self) -> AsyncVehiclesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/DealerMax-app/partnermax-python#accessing-raw-response-data-eg-headers + """ + return AsyncVehiclesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncVehiclesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/DealerMax-app/partnermax-python#with_streaming_response + """ + return AsyncVehiclesResourceWithStreamingResponse(self) + + async def create( + self, + dealer_id: str, + *, + certified_km: int, + cost_price_eur: float, + motornet_code: str, + plate: str, + registration_year: int, + sale_price_eur: float, + alloy_wheel_size: Optional[int] | Omit = omit, + color: Optional[str] | Omit = omit, + description: str | Omit = omit, + extended_warranty_enabled: bool | Omit = omit, + extended_warranty_months: Optional[int] | Omit = omit, + inspection_expiry_date: Union[str, date, None] | Omit = omit, + is_for_sale: bool | Omit = omit, + is_visible: bool | Omit = omit, + last_service_date: Union[str, date, None] | Omit = omit, + last_service_km: Optional[int] | Omit = omit, + last_service_notes: Optional[str] | Omit = omit, + notes: Optional[str] | Omit = omit, + previous_owner_count: Optional[int] | Omit = omit, + previous_ownership_transfer_date: Union[str, date, None] | Omit = omit, + registration_month: Optional[int] | Omit = omit, + road_tax_expiry_date: Union[str, date, None] | Omit = omit, + vat_displayed: bool | Omit = omit, + vehicle_damaged: bool | Omit = omit, + vin: Optional[str] | Omit = omit, + idempotency_key: str | 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, + ) -> VehicleDetail: + """ + Provision a new used vehicle in a dealer's stock. + + Writes are atomic across `azlease_usatoin` and `azlease_usatoauto` using a + `SAVEPOINT` so a UNIQUE plate violation rolls back cleanly. On success the + AI-content worker (:mod:`azurenet-engine.app.jobs.usato_ai_content_worker`) + picks up the new row within 60 seconds and generates the SEO body + pgvector + embedding — at which point the vehicle becomes discoverable on the cross-network + MCP / Custom GPT / NLWeb surfaces. The response returns immediately (no + synchronous wait on the worker). + + Args: + certified_km: Certified odometer reading at intake, in kilometres. + + cost_price_eur: Cost basis to the dealer in EUR (partner/dealer internal). Not surfaced on + consumer-facing AI surfaces; used by dealer reporting and margin analytics only. + + motornet_code: Motornet UNI code identifying the exact vehicle configuration. Must exist in + `mnet_dettagli_usato` at submission time; otherwise the call returns 422 + `motornet_code_not_in_catalogue`. The partner is expected to source this from + its own DMS; partnermax does not expose a plate→code lookup. + + plate: Italian licence plate. Uppercased server-side. UNIQUE across the network for + active vehicles (`visibile=true AND venduto_il IS NULL`); reusable once the + previous holder sells/hides the row. + + registration_year: Year of first registration. Upper bound is current year + 1. + + sale_price_eur: Public sale price in EUR. Surfaced on MCP / Custom GPT / NLWeb and on the + dealer's site JSON-LD `Offer.price`. + + description: Partner-supplied long description. Surfaced on the dealer site detail page. + + is_for_sale: Maps to `azlease_usatoauto.is_vendita_enabled`. When false the row is in stock + but not offered for sale. + + is_visible: Soft-publish flag. When false the row exists in stock but is excluded from + consumer-facing AI surfaces. Maps to `azlease_usatoin.visibile`. + + notes: Free-form short notes; surfaced as `mnet_dettagli.precisazioni`-style. + + previous_ownership_transfer_date: Date of the most recent ownership transfer, if known. + + registration_month: Month of registration (1–12). + + vat_displayed: If true the public price is displayed VAT-exposed (B2B); otherwise VAT-inclusive + (B2C). + + vin: ISO 3779 vehicle identification number. Optional but strongly recommended. + + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return await self._post( + path_template("/v1/dealers/{dealer_id}/vehicles", dealer_id=dealer_id), + body=await async_maybe_transform( + { + "certified_km": certified_km, + "cost_price_eur": cost_price_eur, + "motornet_code": motornet_code, + "plate": plate, + "registration_year": registration_year, + "sale_price_eur": sale_price_eur, + "alloy_wheel_size": alloy_wheel_size, + "color": color, + "description": description, + "extended_warranty_enabled": extended_warranty_enabled, + "extended_warranty_months": extended_warranty_months, + "inspection_expiry_date": inspection_expiry_date, + "is_for_sale": is_for_sale, + "is_visible": is_visible, + "last_service_date": last_service_date, + "last_service_km": last_service_km, + "last_service_notes": last_service_notes, + "notes": notes, + "previous_owner_count": previous_owner_count, + "previous_ownership_transfer_date": previous_ownership_transfer_date, + "registration_month": registration_month, + "road_tax_expiry_date": road_tax_expiry_date, + "vat_displayed": vat_displayed, + "vehicle_damaged": vehicle_damaged, + "vin": vin, + }, + vehicle_create_params.VehicleCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=VehicleDetail, + ) + + async def retrieve( + self, + vehicle_id: str, + *, + dealer_id: str, + include_deleted: bool | 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, + ) -> VehicleDetail: + """ + Get Vehicle + + Args: + include_deleted: If true, the detail of a soft-deleted vehicle is returned. Default false — + soft-deleted rows return 404 to keep behaviour consistent with the list + endpoint. + + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + if not vehicle_id: + raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") + return await self._get( + path_template("/v1/dealers/{dealer_id}/vehicles/{vehicle_id}", dealer_id=dealer_id, vehicle_id=vehicle_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"include_deleted": include_deleted}, vehicle_retrieve_params.VehicleRetrieveParams + ), + ), + cast_to=VehicleDetail, + ) + + async def update( + self, + vehicle_id: str, + *, + dealer_id: str, + alloy_wheel_size: Optional[int] | Omit = omit, + certified_km: Optional[int] | Omit = omit, + color: Optional[str] | Omit = omit, + cost_price_eur: Optional[float] | Omit = omit, + description: Optional[str] | Omit = omit, + extended_warranty_enabled: Optional[bool] | Omit = omit, + extended_warranty_months: Optional[int] | Omit = omit, + inspection_expiry_date: Union[str, date, None] | Omit = omit, + is_for_sale: Optional[bool] | Omit = omit, + is_visible: Optional[bool] | Omit = omit, + last_service_date: Union[str, date, None] | Omit = omit, + last_service_km: Optional[int] | Omit = omit, + last_service_notes: Optional[str] | Omit = omit, + notes: Optional[str] | Omit = omit, + previous_owner_count: Optional[int] | Omit = omit, + previous_ownership_transfer_date: Union[str, date, None] | Omit = omit, + registration_month: Optional[int] | Omit = omit, + road_tax_expiry_date: Union[str, date, None] | Omit = omit, + sale_price_eur: Optional[float] | Omit = omit, + vat_displayed: Optional[bool] | Omit = omit, + vehicle_damaged: Optional[bool] | Omit = omit, + idempotency_key: str | 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, + ) -> VehicleDetail: + """ + Partial update of a vehicle. + + Splits the inbound body across the two physical tables (`azlease_usatoauto` and + `azlease_usatoin`) and emits at most one UPDATE per table inside a single + transaction. Fields not present in the body are not touched. + + Args: + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + if not vehicle_id: + raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") + extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return await self._patch( + path_template("/v1/dealers/{dealer_id}/vehicles/{vehicle_id}", dealer_id=dealer_id, vehicle_id=vehicle_id), + body=await async_maybe_transform( + { + "alloy_wheel_size": alloy_wheel_size, + "certified_km": certified_km, + "color": color, + "cost_price_eur": cost_price_eur, + "description": description, + "extended_warranty_enabled": extended_warranty_enabled, + "extended_warranty_months": extended_warranty_months, + "inspection_expiry_date": inspection_expiry_date, + "is_for_sale": is_for_sale, + "is_visible": is_visible, + "last_service_date": last_service_date, + "last_service_km": last_service_km, + "last_service_notes": last_service_notes, + "notes": notes, + "previous_owner_count": previous_owner_count, + "previous_ownership_transfer_date": previous_ownership_transfer_date, + "registration_month": registration_month, + "road_tax_expiry_date": road_tax_expiry_date, + "sale_price_eur": sale_price_eur, + "vat_displayed": vat_displayed, + "vehicle_damaged": vehicle_damaged, + }, + vehicle_update_params.VehicleUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=VehicleDetail, + ) + + async def list( + self, + dealer_id: str, + *, + cursor: Optional[str] | Omit = omit, + include_deleted: bool | Omit = omit, + is_for_sale: Optional[bool] | Omit = omit, + is_visible: Optional[bool] | Omit = omit, + limit: int | 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, + ) -> VehicleList: + """ + List vehicles in a dealer's stock owned by the calling partner. + + Cursor pagination is opaque base64url over the last vehicle UUID. Default sort + is `i.data_inserimento ASC` so freshly provisioned vehicles surface at the tail. + Soft-deleted rows are excluded unless `include_deleted=true` is set explicitly — + this preserves the soft-delete semantic across the API contract. + + Args: + include_deleted: If true, soft-deleted rows (`venduto_il` populated) are also returned. Default + false — listings hide soft-deleted vehicles. + + is_for_sale: Filter on the sale flag. + + is_visible: Filter on the visibility flag. + + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + return await self._get( + path_template("/v1/dealers/{dealer_id}/vehicles", dealer_id=dealer_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "cursor": cursor, + "include_deleted": include_deleted, + "is_for_sale": is_for_sale, + "is_visible": is_visible, + "limit": limit, + }, + vehicle_list_params.VehicleListParams, + ), + ), + cast_to=VehicleList, + ) + + async def delete( + self, + vehicle_id: str, + *, + dealer_id: str, + # 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, + ) -> None: + """ + Withdraw a vehicle from sale without deleting the row. + + Sets `azlease_usatoin.visibile = FALSE` and stamps `venduto_il = now()`. The + plate becomes reusable on the network the moment this returns (the + active-uniqueness check excludes rows where `visibile = FALSE` OR + `venduto_il IS NOT NULL`). + + Soft-delete is the canonical "remove this vehicle from sale" surface. The + AI-citation consumers (MCP `_tool_search_vehicles`, Custom GPT + `search_vehicles_network`, NLWeb `/ask`) each filter their own queries on + `i.visibile = TRUE AND i.venduto_il IS NULL` — the shared `v_apimax_listing` + view itself does not impose that filter, every consumer adds it. The result on + the partner side is the same: a soft-deleted vehicle disappears from every AI + surface within the next index cycle. + + Returns `409 vehicle_already_deleted` if the row is already soft- deleted — same + idempotency pattern as the dealers DELETE endpoint. + + Args: + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + if not vehicle_id: + raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template("/v1/dealers/{dealer_id}/vehicles/{vehicle_id}", dealer_id=dealer_id, vehicle_id=vehicle_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def bulk( + self, + dealer_id: str, + *, + vehicles: Iterable[vehicle_bulk_params.Vehicle], + idempotency_key: str | 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, + ) -> BulkCreateVehiclesResponse: + """ + Provision up to `BULK_MAX_ROWS` vehicles in a single synchronous call. + + Each row is processed inside its own `SAVEPOINT` so a failure on row N + (validation, plate conflict, motornet not in catalogue, race) is isolated — the + SAVEPOINT rolls back, the per-row outcome is collected with a structured error + code, and the loop continues with row N+1. + + Successful rows accumulate in the outer transaction and are committed together + at the end of the request. Failed rows leave no trace in the database. + + Returns `207 Multi-Status`. The response carries: + + - `total`, `succeeded`, `failed` — aggregate counters for quick branch logic on + the partner side. + - `results` — array of per-row outcomes, indexed by the position in the request + `vehicles[]` array. Successful rows include the full `VehicleDetail`; failed + rows include `error_code` + `error_message` keyed to the same codes as the + single-POST surface so the partner reuses one error handler for both paths. + + For imports larger than `BULK_MAX_ROWS` (currently 100), the partner is expected + to chunk the array client-side. A 5 000-vehicle initial migration is 50 calls; + the partner controls concurrency. + + Args: + vehicles: Array of vehicles to create. Between 1 and 100 rows per call. For larger + imports, the partner is expected to chunk client-side (e.g. 50 calls of 100 rows + each for a 5 000-vehicle migration). + + 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 + """ + if not dealer_id: + raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") + extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return await self._post( + path_template("/v1/dealers/{dealer_id}/vehicles/bulk", dealer_id=dealer_id), + body=await async_maybe_transform({"vehicles": vehicles}, vehicle_bulk_params.VehicleBulkParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BulkCreateVehiclesResponse, + ) + + +class VehiclesResourceWithRawResponse: + def __init__(self, vehicles: VehiclesResource) -> None: + self._vehicles = vehicles + + self.create = to_raw_response_wrapper( + vehicles.create, + ) + self.retrieve = to_raw_response_wrapper( + vehicles.retrieve, + ) + self.update = to_raw_response_wrapper( + vehicles.update, + ) + self.list = to_raw_response_wrapper( + vehicles.list, + ) + self.delete = to_raw_response_wrapper( + vehicles.delete, + ) + self.bulk = to_raw_response_wrapper( + vehicles.bulk, + ) + + +class AsyncVehiclesResourceWithRawResponse: + def __init__(self, vehicles: AsyncVehiclesResource) -> None: + self._vehicles = vehicles + + self.create = async_to_raw_response_wrapper( + vehicles.create, + ) + self.retrieve = async_to_raw_response_wrapper( + vehicles.retrieve, + ) + self.update = async_to_raw_response_wrapper( + vehicles.update, + ) + self.list = async_to_raw_response_wrapper( + vehicles.list, + ) + self.delete = async_to_raw_response_wrapper( + vehicles.delete, + ) + self.bulk = async_to_raw_response_wrapper( + vehicles.bulk, + ) + + +class VehiclesResourceWithStreamingResponse: + def __init__(self, vehicles: VehiclesResource) -> None: + self._vehicles = vehicles + + self.create = to_streamed_response_wrapper( + vehicles.create, + ) + self.retrieve = to_streamed_response_wrapper( + vehicles.retrieve, + ) + self.update = to_streamed_response_wrapper( + vehicles.update, + ) + self.list = to_streamed_response_wrapper( + vehicles.list, + ) + self.delete = to_streamed_response_wrapper( + vehicles.delete, + ) + self.bulk = to_streamed_response_wrapper( + vehicles.bulk, + ) + + +class AsyncVehiclesResourceWithStreamingResponse: + def __init__(self, vehicles: AsyncVehiclesResource) -> None: + self._vehicles = vehicles + + self.create = async_to_streamed_response_wrapper( + vehicles.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + vehicles.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + vehicles.update, + ) + self.list = async_to_streamed_response_wrapper( + vehicles.list, + ) + self.delete = async_to_streamed_response_wrapper( + vehicles.delete, + ) + self.bulk = async_to_streamed_response_wrapper( + vehicles.bulk, + ) diff --git a/src/partnermax/types/dealers/__init__.py b/src/partnermax/types/dealers/__init__.py index d04cf6f..f97da52 100644 --- a/src/partnermax/types/dealers/__init__.py +++ b/src/partnermax/types/dealers/__init__.py @@ -3,6 +3,16 @@ from __future__ import annotations from .nlt_settings import NltSettings as NltSettings +from .vehicle_list import VehicleList as VehicleList +from .vehicle_detail import VehicleDetail as VehicleDetail +from .vehicle_summary import VehicleSummary as VehicleSummary +from .bulk_row_outcome import BulkRowOutcome as BulkRowOutcome from .down_payment_tiers import DownPaymentTiers as DownPaymentTiers +from .vehicle_bulk_params import VehicleBulkParams as VehicleBulkParams +from .vehicle_list_params import VehicleListParams as VehicleListParams +from .vehicle_create_params import VehicleCreateParams as VehicleCreateParams +from .vehicle_update_params import VehicleUpdateParams as VehicleUpdateParams +from .vehicle_retrieve_params import VehicleRetrieveParams as VehicleRetrieveParams from .down_payment_tiers_param import DownPaymentTiersParam as DownPaymentTiersParam from .nlt_setting_update_params import NltSettingUpdateParams as NltSettingUpdateParams +from .bulk_create_vehicles_response import BulkCreateVehiclesResponse as BulkCreateVehiclesResponse diff --git a/src/partnermax/types/dealers/bulk_create_vehicles_response.py b/src/partnermax/types/dealers/bulk_create_vehicles_response.py new file mode 100644 index 0000000..e710615 --- /dev/null +++ b/src/partnermax/types/dealers/bulk_create_vehicles_response.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from ..._models import BaseModel +from .bulk_row_outcome import BulkRowOutcome + +__all__ = ["BulkCreateVehiclesResponse"] + + +class BulkCreateVehiclesResponse(BaseModel): + """Response of ``POST /v1/dealers/{dealer_id}/vehicles/bulk``. + + HTTP status is ``207 Multi-Status`` (rather than 201) to make + partial success explicit at the protocol level. The aggregate + counters at the top let a partner short-circuit when every row + succeeded; the ``results`` array carries per-row detail when not. + """ + + failed: int + + results: List[BulkRowOutcome] + + succeeded: int + + total: int + """Number of rows in the request body.""" diff --git a/src/partnermax/types/dealers/bulk_row_outcome.py b/src/partnermax/types/dealers/bulk_row_outcome.py new file mode 100644 index 0000000..e3b6cb7 --- /dev/null +++ b/src/partnermax/types/dealers/bulk_row_outcome.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel +from .vehicle_detail import VehicleDetail + +__all__ = ["BulkRowOutcome"] + + +class BulkRowOutcome(BaseModel): + """Per-row result inside ``BulkCreateVehiclesResponse``. + + On success ``vehicle`` is populated (full ``VehicleDetail``) and + ``error_code``/``error_message`` are null. On failure ``vehicle`` is + null and ``error_code``/``error_message`` carry the same code that + the corresponding single-create POST would have raised + (e.g. ``motornet_code_not_in_catalogue``, ``vehicle_plate_already_registered``, + ``validation_error``). + """ + + row_index: int + """Zero-based index of the row in the request `vehicles[]` array. + + Stable across retries — the partner can correlate failures back to its own batch + by this index. + """ + + status: Literal["succeeded", "failed"] + + error_code: Optional[str] = None + + error_message: Optional[str] = None + + vehicle: Optional[VehicleDetail] = None + """Full vehicle resource. + + Returned by `GET /v1/dealers/{id}/vehicles/{id}`, + `POST /v1/dealers/{id}/vehicles`, and `PATCH /v1/dealers/{id}/vehicles/{id}`. + + `technical_details` carries the flat Motornet specs dict (Italian column names + as keys: `cilindrata`, `kw`, `hp`, `lunghezza`, `consumo_medio`, + `emissioni_co2`, etc.). Same shape conventions as `NltOfferDetail` + (`feedback_partnermax_field_naming_us_english`: field names are English + snake_case, raw catalogue values stay verbatim). + """ diff --git a/src/partnermax/types/dealers/vehicle_bulk_params.py b/src/partnermax/types/dealers/vehicle_bulk_params.py new file mode 100644 index 0000000..38d9fcb --- /dev/null +++ b/src/partnermax/types/dealers/vehicle_bulk_params.py @@ -0,0 +1,130 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable, Optional +from datetime import date +from typing_extensions import Required, Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["VehicleBulkParams", "Vehicle"] + + +class VehicleBulkParams(TypedDict, total=False): + vehicles: Required[Iterable[Vehicle]] + """Array of vehicles to create. + + Between 1 and 100 rows per call. For larger imports, the partner is expected to + chunk client-side (e.g. 50 calls of 100 rows each for a 5 000-vehicle + migration). + """ + + idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] + + +class Vehicle(TypedDict, total=False): + """Request body for vehicle provisioning. + + The partner sends a small, vehicle-specific payload. All technical specs + (brand, model, trim, fuel type, displacement, dimensions, CO2, etc.) are + derived server-side from `mnet_dettagli` via the `motornet_code` join — + the partner never types them. This is the same canonical pattern used by + NLT offers and matches the platform rule + `feedback_motornet_authoritative`. + + Fields immutable after creation: `motornet_code`, `plate`, `vin`. Other + fields may be updated via PATCH. + """ + + certified_km: Required[int] + """Certified odometer reading at intake, in kilometres.""" + + cost_price_eur: Required[float] + """Cost basis to the dealer in EUR (partner/dealer internal). + + Not surfaced on consumer-facing AI surfaces; used by dealer reporting and margin + analytics only. + """ + + motornet_code: Required[str] + """Motornet UNI code identifying the exact vehicle configuration. + + Must exist in `mnet_dettagli_usato` at submission time; otherwise the call + returns 422 `motornet_code_not_in_catalogue`. The partner is expected to source + this from its own DMS; partnermax does not expose a plate→code lookup. + """ + + plate: Required[str] + """Italian licence plate. + + Uppercased server-side. UNIQUE across the network for active vehicles + (`visibile=true AND venduto_il IS NULL`); reusable once the previous holder + sells/hides the row. + """ + + registration_year: Required[int] + """Year of first registration. Upper bound is current year + 1.""" + + sale_price_eur: Required[float] + """Public sale price in EUR. + + Surfaced on MCP / Custom GPT / NLWeb and on the dealer's site JSON-LD + `Offer.price`. + """ + + alloy_wheel_size: Optional[int] + + color: Optional[str] + + description: str + """Partner-supplied long description. Surfaced on the dealer site detail page.""" + + extended_warranty_enabled: bool + + extended_warranty_months: Optional[int] + + inspection_expiry_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + + is_for_sale: bool + """Maps to `azlease_usatoauto.is_vendita_enabled`. + + When false the row is in stock but not offered for sale. + """ + + is_visible: bool + """Soft-publish flag. + + When false the row exists in stock but is excluded from consumer-facing AI + surfaces. Maps to `azlease_usatoin.visibile`. + """ + + last_service_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + + last_service_km: Optional[int] + + last_service_notes: Optional[str] + + notes: Optional[str] + """Free-form short notes; surfaced as `mnet_dettagli.precisazioni`-style.""" + + previous_owner_count: Optional[int] + + previous_ownership_transfer_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + """Date of the most recent ownership transfer, if known.""" + + registration_month: Optional[int] + """Month of registration (1–12).""" + + road_tax_expiry_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + + vat_displayed: bool + """ + If true the public price is displayed VAT-exposed (B2B); otherwise VAT-inclusive + (B2C). + """ + + vehicle_damaged: bool + + vin: Optional[str] + """ISO 3779 vehicle identification number. Optional but strongly recommended.""" diff --git a/src/partnermax/types/dealers/vehicle_create_params.py b/src/partnermax/types/dealers/vehicle_create_params.py new file mode 100644 index 0000000..ed80776 --- /dev/null +++ b/src/partnermax/types/dealers/vehicle_create_params.py @@ -0,0 +1,107 @@ +# 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 date +from typing_extensions import Required, Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["VehicleCreateParams"] + + +class VehicleCreateParams(TypedDict, total=False): + certified_km: Required[int] + """Certified odometer reading at intake, in kilometres.""" + + cost_price_eur: Required[float] + """Cost basis to the dealer in EUR (partner/dealer internal). + + Not surfaced on consumer-facing AI surfaces; used by dealer reporting and margin + analytics only. + """ + + motornet_code: Required[str] + """Motornet UNI code identifying the exact vehicle configuration. + + Must exist in `mnet_dettagli_usato` at submission time; otherwise the call + returns 422 `motornet_code_not_in_catalogue`. The partner is expected to source + this from its own DMS; partnermax does not expose a plate→code lookup. + """ + + plate: Required[str] + """Italian licence plate. + + Uppercased server-side. UNIQUE across the network for active vehicles + (`visibile=true AND venduto_il IS NULL`); reusable once the previous holder + sells/hides the row. + """ + + registration_year: Required[int] + """Year of first registration. Upper bound is current year + 1.""" + + sale_price_eur: Required[float] + """Public sale price in EUR. + + Surfaced on MCP / Custom GPT / NLWeb and on the dealer's site JSON-LD + `Offer.price`. + """ + + alloy_wheel_size: Optional[int] + + color: Optional[str] + + description: str + """Partner-supplied long description. Surfaced on the dealer site detail page.""" + + extended_warranty_enabled: bool + + extended_warranty_months: Optional[int] + + inspection_expiry_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + + is_for_sale: bool + """Maps to `azlease_usatoauto.is_vendita_enabled`. + + When false the row is in stock but not offered for sale. + """ + + is_visible: bool + """Soft-publish flag. + + When false the row exists in stock but is excluded from consumer-facing AI + surfaces. Maps to `azlease_usatoin.visibile`. + """ + + last_service_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + + last_service_km: Optional[int] + + last_service_notes: Optional[str] + + notes: Optional[str] + """Free-form short notes; surfaced as `mnet_dettagli.precisazioni`-style.""" + + previous_owner_count: Optional[int] + + previous_ownership_transfer_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + """Date of the most recent ownership transfer, if known.""" + + registration_month: Optional[int] + """Month of registration (1–12).""" + + road_tax_expiry_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + + vat_displayed: bool + """ + If true the public price is displayed VAT-exposed (B2B); otherwise VAT-inclusive + (B2C). + """ + + vehicle_damaged: bool + + vin: Optional[str] + """ISO 3779 vehicle identification number. Optional but strongly recommended.""" + + idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] diff --git a/src/partnermax/types/dealers/vehicle_detail.py b/src/partnermax/types/dealers/vehicle_detail.py new file mode 100644 index 0000000..e25fce1 --- /dev/null +++ b/src/partnermax/types/dealers/vehicle_detail.py @@ -0,0 +1,100 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from datetime import date, datetime + +from ..._models import BaseModel + +__all__ = ["VehicleDetail"] + + +class VehicleDetail(BaseModel): + """Full vehicle resource. + + Returned by `GET /v1/dealers/{id}/vehicles/{id}`, + `POST /v1/dealers/{id}/vehicles`, and `PATCH /v1/dealers/{id}/vehicles/{id}`. + + `technical_details` carries the flat Motornet specs dict (Italian column + names as keys: `cilindrata`, `kw`, `hp`, `lunghezza`, `consumo_medio`, + `emissioni_co2`, etc.). Same shape conventions as `NltOfferDetail` + (`feedback_partnermax_field_naming_us_english`: field names are + English snake_case, raw catalogue values stay verbatim). + """ + + certified_km: int + + cost_price_eur: float + + created_at: datetime + + dealer_id: str + + description: str + + extended_warranty_enabled: bool + + is_for_sale: bool + + is_visible: bool + + last_modified_at: datetime + + motornet_code: str + + partner_id: str + + plate: str + + registration_year: int + + sale_price_eur: float + + vat_displayed: bool + + vehicle_damaged: bool + + vehicle_id: str + + alloy_wheel_size: Optional[int] = None + + brand: Optional[str] = None + + color: Optional[str] = None + + extended_warranty_months: Optional[int] = None + + fuel_type: Optional[str] = None + + image_urls: Optional[List[str]] = None + """Vehicle photos. Empty in v1 (media upload ships in v1.2).""" + + inspection_expiry_date: Optional[date] = None + + last_service_date: Optional[date] = None + + last_service_km: Optional[int] = None + + last_service_notes: Optional[str] = None + + model: Optional[str] = None + + notes: Optional[str] = None + + previous_owner_count: Optional[int] = None + + previous_ownership_transfer_date: Optional[date] = None + + registration_month: Optional[int] = None + + road_tax_expiry_date: Optional[date] = None + + technical_details: Optional[Dict[str, object]] = None + """ + Flat dict of every non-null `mnet_dettagli_usato` column for this + `motornet_code`. Keys stay in Italian because they are raw SQL column names; + native units preserved (mm, kg, kW, CV, g/km, etc.). + """ + + trim: Optional[str] = None + + vin: Optional[str] = None diff --git a/src/partnermax/types/dealers/vehicle_list.py b/src/partnermax/types/dealers/vehicle_list.py new file mode 100644 index 0000000..94c68a9 --- /dev/null +++ b/src/partnermax/types/dealers/vehicle_list.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from .vehicle_summary import VehicleSummary + +__all__ = ["VehicleList"] + + +class VehicleList(BaseModel): + """Cursor-paginated list of vehicle summaries.""" + + data: List[VehicleSummary] + + has_more: bool + + next_cursor: Optional[str] = None diff --git a/src/partnermax/types/dealers/vehicle_list_params.py b/src/partnermax/types/dealers/vehicle_list_params.py new file mode 100644 index 0000000..3380df1 --- /dev/null +++ b/src/partnermax/types/dealers/vehicle_list_params.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["VehicleListParams"] + + +class VehicleListParams(TypedDict, total=False): + cursor: Optional[str] + + include_deleted: bool + """If true, soft-deleted rows (`venduto_il` populated) are also returned. + + Default false — listings hide soft-deleted vehicles. + """ + + is_for_sale: Optional[bool] + """Filter on the sale flag.""" + + is_visible: Optional[bool] + """Filter on the visibility flag.""" + + limit: int diff --git a/src/partnermax/types/dealers/vehicle_retrieve_params.py b/src/partnermax/types/dealers/vehicle_retrieve_params.py new file mode 100644 index 0000000..7cb721d --- /dev/null +++ b/src/partnermax/types/dealers/vehicle_retrieve_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["VehicleRetrieveParams"] + + +class VehicleRetrieveParams(TypedDict, total=False): + dealer_id: Required[str] + + include_deleted: bool + """If true, the detail of a soft-deleted vehicle is returned. + + Default false — soft-deleted rows return 404 to keep behaviour consistent with + the list endpoint. + """ diff --git a/src/partnermax/types/dealers/vehicle_summary.py b/src/partnermax/types/dealers/vehicle_summary.py new file mode 100644 index 0000000..50d90f2 --- /dev/null +++ b/src/partnermax/types/dealers/vehicle_summary.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["VehicleSummary"] + + +class VehicleSummary(BaseModel): + """Compact vehicle payload for list endpoints. + + Catalogue fields (`brand`, `model`, `trim`, `fuel_type`) are derived from + `mnet_dettagli` at read time. Italian raw labels are surfaced verbatim — + same convention as NLT (`apimax`-aligned). + """ + + certified_km: int + + created_at: datetime + + dealer_id: str + + is_for_sale: bool + + is_visible: bool + + motornet_code: str + + plate: str + + registration_year: int + + sale_price_eur: float + + vehicle_id: str + + brand: Optional[str] = None + + color: Optional[str] = None + + fuel_type: Optional[str] = None + + model: Optional[str] = None + + trim: Optional[str] = None diff --git a/src/partnermax/types/dealers/vehicle_update_params.py b/src/partnermax/types/dealers/vehicle_update_params.py new file mode 100644 index 0000000..5707c59 --- /dev/null +++ b/src/partnermax/types/dealers/vehicle_update_params.py @@ -0,0 +1,59 @@ +# 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 date +from typing_extensions import Required, Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["VehicleUpdateParams"] + + +class VehicleUpdateParams(TypedDict, total=False): + dealer_id: Required[str] + + alloy_wheel_size: Optional[int] + + certified_km: Optional[int] + + color: Optional[str] + + cost_price_eur: Optional[float] + + description: Optional[str] + + extended_warranty_enabled: Optional[bool] + + extended_warranty_months: Optional[int] + + inspection_expiry_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + + is_for_sale: Optional[bool] + + is_visible: Optional[bool] + + last_service_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + + last_service_km: Optional[int] + + last_service_notes: Optional[str] + + notes: Optional[str] + + previous_owner_count: Optional[int] + + previous_ownership_transfer_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + + registration_month: Optional[int] + + road_tax_expiry_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + + sale_price_eur: Optional[float] + + vat_displayed: Optional[bool] + + vehicle_damaged: Optional[bool] + + idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] diff --git a/tests/api_resources/dealers/test_vehicles.py b/tests/api_resources/dealers/test_vehicles.py new file mode 100644 index 0000000..53f0af6 --- /dev/null +++ b/tests/api_resources/dealers/test_vehicles.py @@ -0,0 +1,973 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from partnermax import Partnermax, AsyncPartnermax +from tests.utils import assert_matches_type +from partnermax._utils import parse_date +from partnermax.types.dealers import ( + VehicleList, + VehicleDetail, + BulkCreateVehiclesResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestVehicles: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: Partnermax) -> None: + vehicle = client.dealers.vehicles.create( + dealer_id="dealer_id", + certified_km=0, + cost_price_eur=100, + motornet_code="xxxx", + plate="26F1KLZN", + registration_year=1960, + sale_price_eur=100, + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Partnermax) -> None: + vehicle = client.dealers.vehicles.create( + dealer_id="dealer_id", + certified_km=0, + cost_price_eur=100, + motornet_code="xxxx", + plate="26F1KLZN", + registration_year=1960, + sale_price_eur=100, + alloy_wheel_size=13, + color="color", + description="description", + extended_warranty_enabled=True, + extended_warranty_months=1, + inspection_expiry_date=parse_date("2019-12-27"), + is_for_sale=True, + is_visible=True, + last_service_date=parse_date("2019-12-27"), + last_service_km=0, + last_service_notes="last_service_notes", + notes="notes", + previous_owner_count=0, + previous_ownership_transfer_date=parse_date("2019-12-27"), + registration_month=1, + road_tax_expiry_date=parse_date("2019-12-27"), + vat_displayed=True, + vehicle_damaged=True, + vin="PTNLCJPPNYGP316PJ", + idempotency_key="Idempotency-Key", + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: Partnermax) -> None: + response = client.dealers.vehicles.with_raw_response.create( + dealer_id="dealer_id", + certified_km=0, + cost_price_eur=100, + motornet_code="xxxx", + plate="26F1KLZN", + registration_year=1960, + sale_price_eur=100, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Partnermax) -> None: + with client.dealers.vehicles.with_streaming_response.create( + dealer_id="dealer_id", + certified_km=0, + cost_price_eur=100, + motornet_code="xxxx", + plate="26F1KLZN", + registration_year=1960, + sale_price_eur=100, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_create(self, client: Partnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.vehicles.with_raw_response.create( + dealer_id="", + certified_km=0, + cost_price_eur=100, + motornet_code="xxxx", + plate="26F1KLZN", + registration_year=1960, + sale_price_eur=100, + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Partnermax) -> None: + vehicle = client.dealers.vehicles.retrieve( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve_with_all_params(self, client: Partnermax) -> None: + vehicle = client.dealers.vehicles.retrieve( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + include_deleted=True, + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Partnermax) -> None: + response = client.dealers.vehicles.with_raw_response.retrieve( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Partnermax) -> None: + with client.dealers.vehicles.with_streaming_response.retrieve( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Partnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.vehicles.with_raw_response.retrieve( + vehicle_id="vehicle_id", + dealer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): + client.dealers.vehicles.with_raw_response.retrieve( + vehicle_id="", + dealer_id="dealer_id", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Partnermax) -> None: + vehicle = client.dealers.vehicles.update( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Partnermax) -> None: + vehicle = client.dealers.vehicles.update( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + alloy_wheel_size=13, + certified_km=0, + color="color", + cost_price_eur=100, + description="description", + extended_warranty_enabled=True, + extended_warranty_months=1, + inspection_expiry_date=parse_date("2019-12-27"), + is_for_sale=True, + is_visible=True, + last_service_date=parse_date("2019-12-27"), + last_service_km=0, + last_service_notes="last_service_notes", + notes="notes", + previous_owner_count=0, + previous_ownership_transfer_date=parse_date("2019-12-27"), + registration_month=1, + road_tax_expiry_date=parse_date("2019-12-27"), + sale_price_eur=100, + vat_displayed=True, + vehicle_damaged=True, + idempotency_key="Idempotency-Key", + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Partnermax) -> None: + response = client.dealers.vehicles.with_raw_response.update( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Partnermax) -> None: + with client.dealers.vehicles.with_streaming_response.update( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Partnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.vehicles.with_raw_response.update( + vehicle_id="vehicle_id", + dealer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): + client.dealers.vehicles.with_raw_response.update( + vehicle_id="", + dealer_id="dealer_id", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Partnermax) -> None: + vehicle = client.dealers.vehicles.list( + dealer_id="dealer_id", + ) + assert_matches_type(VehicleList, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Partnermax) -> None: + vehicle = client.dealers.vehicles.list( + dealer_id="dealer_id", + cursor="cursor", + include_deleted=True, + is_for_sale=True, + is_visible=True, + limit=1, + ) + assert_matches_type(VehicleList, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Partnermax) -> None: + response = client.dealers.vehicles.with_raw_response.list( + dealer_id="dealer_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = response.parse() + assert_matches_type(VehicleList, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Partnermax) -> None: + with client.dealers.vehicles.with_streaming_response.list( + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = response.parse() + assert_matches_type(VehicleList, vehicle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list(self, client: Partnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.vehicles.with_raw_response.list( + dealer_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_delete(self, client: Partnermax) -> None: + vehicle = client.dealers.vehicles.delete( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + assert vehicle is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Partnermax) -> None: + response = client.dealers.vehicles.with_raw_response.delete( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = response.parse() + assert vehicle is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Partnermax) -> None: + with client.dealers.vehicles.with_streaming_response.delete( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = response.parse() + assert vehicle is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_delete(self, client: Partnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.vehicles.with_raw_response.delete( + vehicle_id="vehicle_id", + dealer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): + client.dealers.vehicles.with_raw_response.delete( + vehicle_id="", + dealer_id="dealer_id", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_bulk(self, client: Partnermax) -> None: + vehicle = client.dealers.vehicles.bulk( + dealer_id="dealer_id", + vehicles=[ + { + "certified_km": 0, + "cost_price_eur": 100, + "motornet_code": "xxxx", + "plate": "26F1KLZN", + "registration_year": 1960, + "sale_price_eur": 100, + } + ], + ) + assert_matches_type(BulkCreateVehiclesResponse, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_bulk_with_all_params(self, client: Partnermax) -> None: + vehicle = client.dealers.vehicles.bulk( + dealer_id="dealer_id", + vehicles=[ + { + "certified_km": 0, + "cost_price_eur": 100, + "motornet_code": "xxxx", + "plate": "26F1KLZN", + "registration_year": 1960, + "sale_price_eur": 100, + "alloy_wheel_size": 13, + "color": "color", + "description": "description", + "extended_warranty_enabled": True, + "extended_warranty_months": 1, + "inspection_expiry_date": parse_date("2019-12-27"), + "is_for_sale": True, + "is_visible": True, + "last_service_date": parse_date("2019-12-27"), + "last_service_km": 0, + "last_service_notes": "last_service_notes", + "notes": "notes", + "previous_owner_count": 0, + "previous_ownership_transfer_date": parse_date("2019-12-27"), + "registration_month": 1, + "road_tax_expiry_date": parse_date("2019-12-27"), + "vat_displayed": True, + "vehicle_damaged": True, + "vin": "PTNLCJPPNYGP316PJ", + } + ], + idempotency_key="Idempotency-Key", + ) + assert_matches_type(BulkCreateVehiclesResponse, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_bulk(self, client: Partnermax) -> None: + response = client.dealers.vehicles.with_raw_response.bulk( + dealer_id="dealer_id", + vehicles=[ + { + "certified_km": 0, + "cost_price_eur": 100, + "motornet_code": "xxxx", + "plate": "26F1KLZN", + "registration_year": 1960, + "sale_price_eur": 100, + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = response.parse() + assert_matches_type(BulkCreateVehiclesResponse, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_bulk(self, client: Partnermax) -> None: + with client.dealers.vehicles.with_streaming_response.bulk( + dealer_id="dealer_id", + vehicles=[ + { + "certified_km": 0, + "cost_price_eur": 100, + "motornet_code": "xxxx", + "plate": "26F1KLZN", + "registration_year": 1960, + "sale_price_eur": 100, + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = response.parse() + assert_matches_type(BulkCreateVehiclesResponse, vehicle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_bulk(self, client: Partnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.vehicles.with_raw_response.bulk( + dealer_id="", + vehicles=[ + { + "certified_km": 0, + "cost_price_eur": 100, + "motornet_code": "xxxx", + "plate": "26F1KLZN", + "registration_year": 1960, + "sale_price_eur": 100, + } + ], + ) + + +class TestAsyncVehicles: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncPartnermax) -> None: + vehicle = await async_client.dealers.vehicles.create( + dealer_id="dealer_id", + certified_km=0, + cost_price_eur=100, + motornet_code="xxxx", + plate="26F1KLZN", + registration_year=1960, + sale_price_eur=100, + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncPartnermax) -> None: + vehicle = await async_client.dealers.vehicles.create( + dealer_id="dealer_id", + certified_km=0, + cost_price_eur=100, + motornet_code="xxxx", + plate="26F1KLZN", + registration_year=1960, + sale_price_eur=100, + alloy_wheel_size=13, + color="color", + description="description", + extended_warranty_enabled=True, + extended_warranty_months=1, + inspection_expiry_date=parse_date("2019-12-27"), + is_for_sale=True, + is_visible=True, + last_service_date=parse_date("2019-12-27"), + last_service_km=0, + last_service_notes="last_service_notes", + notes="notes", + previous_owner_count=0, + previous_ownership_transfer_date=parse_date("2019-12-27"), + registration_month=1, + road_tax_expiry_date=parse_date("2019-12-27"), + vat_displayed=True, + vehicle_damaged=True, + vin="PTNLCJPPNYGP316PJ", + idempotency_key="Idempotency-Key", + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncPartnermax) -> None: + response = await async_client.dealers.vehicles.with_raw_response.create( + dealer_id="dealer_id", + certified_km=0, + cost_price_eur=100, + motornet_code="xxxx", + plate="26F1KLZN", + registration_year=1960, + sale_price_eur=100, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = await response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncPartnermax) -> None: + async with async_client.dealers.vehicles.with_streaming_response.create( + dealer_id="dealer_id", + certified_km=0, + cost_price_eur=100, + motornet_code="xxxx", + plate="26F1KLZN", + registration_year=1960, + sale_price_eur=100, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = await response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_create(self, async_client: AsyncPartnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.vehicles.with_raw_response.create( + dealer_id="", + certified_km=0, + cost_price_eur=100, + motornet_code="xxxx", + plate="26F1KLZN", + registration_year=1960, + sale_price_eur=100, + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncPartnermax) -> None: + vehicle = await async_client.dealers.vehicles.retrieve( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve_with_all_params(self, async_client: AsyncPartnermax) -> None: + vehicle = await async_client.dealers.vehicles.retrieve( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + include_deleted=True, + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncPartnermax) -> None: + response = await async_client.dealers.vehicles.with_raw_response.retrieve( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = await response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncPartnermax) -> None: + async with async_client.dealers.vehicles.with_streaming_response.retrieve( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = await response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncPartnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.vehicles.with_raw_response.retrieve( + vehicle_id="vehicle_id", + dealer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): + await async_client.dealers.vehicles.with_raw_response.retrieve( + vehicle_id="", + dealer_id="dealer_id", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncPartnermax) -> None: + vehicle = await async_client.dealers.vehicles.update( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncPartnermax) -> None: + vehicle = await async_client.dealers.vehicles.update( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + alloy_wheel_size=13, + certified_km=0, + color="color", + cost_price_eur=100, + description="description", + extended_warranty_enabled=True, + extended_warranty_months=1, + inspection_expiry_date=parse_date("2019-12-27"), + is_for_sale=True, + is_visible=True, + last_service_date=parse_date("2019-12-27"), + last_service_km=0, + last_service_notes="last_service_notes", + notes="notes", + previous_owner_count=0, + previous_ownership_transfer_date=parse_date("2019-12-27"), + registration_month=1, + road_tax_expiry_date=parse_date("2019-12-27"), + sale_price_eur=100, + vat_displayed=True, + vehicle_damaged=True, + idempotency_key="Idempotency-Key", + ) + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncPartnermax) -> None: + response = await async_client.dealers.vehicles.with_raw_response.update( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = await response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncPartnermax) -> None: + async with async_client.dealers.vehicles.with_streaming_response.update( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = await response.parse() + assert_matches_type(VehicleDetail, vehicle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncPartnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.vehicles.with_raw_response.update( + vehicle_id="vehicle_id", + dealer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): + await async_client.dealers.vehicles.with_raw_response.update( + vehicle_id="", + dealer_id="dealer_id", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncPartnermax) -> None: + vehicle = await async_client.dealers.vehicles.list( + dealer_id="dealer_id", + ) + assert_matches_type(VehicleList, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncPartnermax) -> None: + vehicle = await async_client.dealers.vehicles.list( + dealer_id="dealer_id", + cursor="cursor", + include_deleted=True, + is_for_sale=True, + is_visible=True, + limit=1, + ) + assert_matches_type(VehicleList, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncPartnermax) -> None: + response = await async_client.dealers.vehicles.with_raw_response.list( + dealer_id="dealer_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = await response.parse() + assert_matches_type(VehicleList, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncPartnermax) -> None: + async with async_client.dealers.vehicles.with_streaming_response.list( + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = await response.parse() + assert_matches_type(VehicleList, vehicle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list(self, async_client: AsyncPartnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.vehicles.with_raw_response.list( + dealer_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncPartnermax) -> None: + vehicle = await async_client.dealers.vehicles.delete( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + assert vehicle is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncPartnermax) -> None: + response = await async_client.dealers.vehicles.with_raw_response.delete( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = await response.parse() + assert vehicle is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncPartnermax) -> None: + async with async_client.dealers.vehicles.with_streaming_response.delete( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = await response.parse() + assert vehicle is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncPartnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.vehicles.with_raw_response.delete( + vehicle_id="vehicle_id", + dealer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): + await async_client.dealers.vehicles.with_raw_response.delete( + vehicle_id="", + dealer_id="dealer_id", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_bulk(self, async_client: AsyncPartnermax) -> None: + vehicle = await async_client.dealers.vehicles.bulk( + dealer_id="dealer_id", + vehicles=[ + { + "certified_km": 0, + "cost_price_eur": 100, + "motornet_code": "xxxx", + "plate": "26F1KLZN", + "registration_year": 1960, + "sale_price_eur": 100, + } + ], + ) + assert_matches_type(BulkCreateVehiclesResponse, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_bulk_with_all_params(self, async_client: AsyncPartnermax) -> None: + vehicle = await async_client.dealers.vehicles.bulk( + dealer_id="dealer_id", + vehicles=[ + { + "certified_km": 0, + "cost_price_eur": 100, + "motornet_code": "xxxx", + "plate": "26F1KLZN", + "registration_year": 1960, + "sale_price_eur": 100, + "alloy_wheel_size": 13, + "color": "color", + "description": "description", + "extended_warranty_enabled": True, + "extended_warranty_months": 1, + "inspection_expiry_date": parse_date("2019-12-27"), + "is_for_sale": True, + "is_visible": True, + "last_service_date": parse_date("2019-12-27"), + "last_service_km": 0, + "last_service_notes": "last_service_notes", + "notes": "notes", + "previous_owner_count": 0, + "previous_ownership_transfer_date": parse_date("2019-12-27"), + "registration_month": 1, + "road_tax_expiry_date": parse_date("2019-12-27"), + "vat_displayed": True, + "vehicle_damaged": True, + "vin": "PTNLCJPPNYGP316PJ", + } + ], + idempotency_key="Idempotency-Key", + ) + assert_matches_type(BulkCreateVehiclesResponse, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_bulk(self, async_client: AsyncPartnermax) -> None: + response = await async_client.dealers.vehicles.with_raw_response.bulk( + dealer_id="dealer_id", + vehicles=[ + { + "certified_km": 0, + "cost_price_eur": 100, + "motornet_code": "xxxx", + "plate": "26F1KLZN", + "registration_year": 1960, + "sale_price_eur": 100, + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vehicle = await response.parse() + assert_matches_type(BulkCreateVehiclesResponse, vehicle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_bulk(self, async_client: AsyncPartnermax) -> None: + async with async_client.dealers.vehicles.with_streaming_response.bulk( + dealer_id="dealer_id", + vehicles=[ + { + "certified_km": 0, + "cost_price_eur": 100, + "motornet_code": "xxxx", + "plate": "26F1KLZN", + "registration_year": 1960, + "sale_price_eur": 100, + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vehicle = await response.parse() + assert_matches_type(BulkCreateVehiclesResponse, vehicle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_bulk(self, async_client: AsyncPartnermax) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.vehicles.with_raw_response.bulk( + dealer_id="", + vehicles=[ + { + "certified_km": 0, + "cost_price_eur": 100, + "motornet_code": "xxxx", + "plate": "26F1KLZN", + "registration_year": 1960, + "sale_price_eur": 100, + } + ], + ) From 2ffea1dd0f768c2832714faedf1244d9641bb337 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:18:40 +0000 Subject: [PATCH 3/3] release: 0.8.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- src/partnermax/_version.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1b77f50..6538ca9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.0" + ".": "0.8.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a5e6b..8af65ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.8.0 (2026-05-18) + +Full Changelog: [v0.7.0...v0.8.0](https://github.com/DealerMax-app/partnermax-python/compare/v0.7.0...v0.8.0) + +### Features + +* **vehicles:** used-vehicle CRUD + bulk insert for v1.1.0 ([c817349](https://github.com/DealerMax-app/partnermax-python/commit/c817349ace294552a56e191f975a444a92b82548)) +* **vehicles:** used-vehicle CRUD + bulk insert for v1.1.0 ([611cb75](https://github.com/DealerMax-app/partnermax-python/commit/611cb752d4d71443a5f1c6c22d24394a87ffa295)) + ## 0.7.0 (2026-05-18) Full Changelog: [v0.6.0...v0.7.0](https://github.com/DealerMax-app/partnermax-python/compare/v0.6.0...v0.7.0) diff --git a/pyproject.toml b/pyproject.toml index 5cb85c9..0f907fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "partnermax" -version = "0.7.0" +version = "0.8.0" description = "The official Python library for the partnermax API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/partnermax/_version.py b/src/partnermax/_version.py index 7663be0..06afba4 100644 --- a/src/partnermax/_version.py +++ b/src/partnermax/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "partnermax" -__version__ = "0.7.0" # x-release-please-version +__version__ = "0.8.0" # x-release-please-version