diff --git a/etsy_python/v3/enums/Listing.py b/etsy_python/v3/enums/Listing.py index d852c3e..b8aebf3 100644 --- a/etsy_python/v3/enums/Listing.py +++ b/etsy_python/v3/enums/Listing.py @@ -93,6 +93,7 @@ class Includes(Enum): INVENTORY = "Inventory" VIDEOS = "Videos" PERSONALIZATION = "Personalization" + BUYER_PRICE = "BuyerPrice" class InventoryIncludes(Enum): diff --git a/etsy_python/v3/resources/Listing.py b/etsy_python/v3/resources/Listing.py index a0b45f1..c395700 100644 --- a/etsy_python/v3/resources/Listing.py +++ b/etsy_python/v3/resources/Listing.py @@ -92,6 +92,8 @@ def find_all_listings_active( shop_location: Optional[str] = None, is_safe: Optional[bool] = None, legacy: Optional[bool] = None, + buyer_country: Optional[str] = None, + currency: Optional[str] = None, ) -> Union[Response, RequestException]: endpoint = "/listings/active" query_params: Dict[str, Any] = { @@ -106,6 +108,8 @@ def find_all_listings_active( "shop_location": shop_location, "is_safe": is_safe, "legacy": legacy, + "buyer_country": buyer_country, + "currency": currency, } return self.session.make_request(endpoint, query_params=query_params) @@ -135,6 +139,8 @@ def get_listings_by_listing_ids( listing_ids: List[int], includes: Optional[List[Includes]] = None, legacy: Optional[bool] = None, + buyer_country: Optional[str] = None, + currency: Optional[str] = None, ) -> Union[Response, RequestException]: endpoint = "/listings/batch" query_params: Dict[str, Any] = { @@ -143,6 +149,8 @@ def get_listings_by_listing_ids( if includes is not None else None, "legacy": legacy, + "buyer_country": buyer_country, + "currency": currency, } return self.session.make_request(endpoint, query_params=query_params) diff --git a/specs/baseline.json b/specs/baseline.json index b4a8806..328e267 100644 --- a/specs/baseline.json +++ b/specs/baseline.json @@ -750,7 +750,8 @@ "Translations", "Inventory", "Videos", - "Personalization" + "Personalization", + "BuyerPrice" ] }, "default": null @@ -961,7 +962,8 @@ "Translations", "Inventory", "Videos", - "Personalization" + "Personalization", + "BuyerPrice" ] }, "default": null @@ -1657,6 +1659,29 @@ "type": "boolean", "description": "When true, filters out mature/adult content from search results." } + }, + { + "name": "currency", + "in": "query", + "description": "The ISO 4217 alphabetic currency code (e.g., EUR, MXN) for price conversion. If provided, the listing price will be converted to this currency.", + "required": false, + "schema": { + "type": "string", + "description": "The ISO 4217 alphabetic currency code (e.g., EUR, MXN) for price conversion. If provided, the listing price will be converted to this currency.", + "default": null + } + }, + { + "name": "buyer_country", + "in": "query", + "description": "The ISO 3166-1 alpha-2 country code (e.g., DE, MX). Filters results to listings that ship to this country.", + "required": false, + "schema": { + "type": "string", + "description": "The ISO 3166-1 alpha-2 country code (e.g., DE, MX). Filters results to listings that ship to this country.", + "format": "ISO 3166-1 alpha-2", + "default": null + } } ], "responses": { @@ -2884,7 +2909,8 @@ "Translations", "Inventory", "Videos", - "Personalization" + "Personalization", + "BuyerPrice" ] }, "default": null @@ -2899,6 +2925,29 @@ "type": "boolean", "description": "This parameter needed to enable new parameters and response values related to processing profiles." } + }, + { + "name": "currency", + "in": "query", + "description": "The ISO 4217 alphabetic currency code (e.g., EUR, MXN) for price conversion. If provided, the listing price will be converted to this currency.", + "required": false, + "schema": { + "type": "string", + "description": "The ISO 4217 alphabetic currency code (e.g., EUR, MXN) for price conversion. If provided, the listing price will be converted to this currency.", + "default": null + } + }, + { + "name": "buyer_country", + "in": "query", + "description": "The ISO 3166-1 alpha-2 country code (e.g., GB, DE). Used for buyer-facing price calculations (VAT, inclusive shipping). Does not filter listings.", + "required": false, + "schema": { + "type": "string", + "description": "The ISO 3166-1 alpha-2 country code (e.g., GB, DE). Used for buyer-facing price calculations (VAT, inclusive shipping). Does not filter listings.", + "format": "ISO 3166-1 alpha-2", + "default": null + } } ], "responses": { @@ -8188,15 +8237,7 @@ } } } - }, - "security": [ - { - "api_key": [], - "oauth2": [ - "shops_r" - ] - } - ] + } } }, "/v3/application/shops/{shop_id}/policies/return/{return_policy_id}": { @@ -8369,15 +8410,7 @@ } } } - }, - "security": [ - { - "api_key": [], - "oauth2": [ - "shops_r" - ] - } - ] + } }, "put": { "operationId": "updateShopReturnPolicy", @@ -12941,6 +12974,15 @@ } ] }, + "converted_price": { + "description": "The listing price converted to the currency requested via the currency parameter. Only present when the currency parameter is provided. Null if the conversion rate is unavailable.", + "oneOf": [ + { + "$ref": "#/components/schemas/Money" + } + ], + "nullable": true + }, "taxonomy_id": { "type": "integer", "description": "The numerical taxonomy ID of the listing. See [SellerTaxonomy](/documentation/reference#tag/SellerTaxonomy) and [BuyerTaxonomy](/documentation/reference#tag/BuyerTaxonomy) for more information.", @@ -13388,6 +13430,15 @@ } ] }, + "converted_price": { + "description": "The listing price converted to the currency requested via the currency parameter. Only present when the currency parameter is provided. Null if the conversion rate is unavailable.", + "oneOf": [ + { + "$ref": "#/components/schemas/Money" + } + ], + "nullable": true + }, "taxonomy_id": { "type": "integer", "description": "The numerical taxonomy ID of the listing. See [SellerTaxonomy](/documentation/reference#tag/SellerTaxonomy) and [BuyerTaxonomy](/documentation/reference#tag/BuyerTaxonomy) for more information.", @@ -13504,6 +13555,15 @@ } ], "nullable": true + }, + "buyer_price": { + "description": "The buyer-facing price for a listing, including VAT, inclusive shipping (UK), and active promotions. Requires buyer_country parameter. Shows base_price, shipping_cost, original_price (display price), and discounted_price if a promotion is active. Currently only supported on the /listings/batch endpoint.", + "oneOf": [ + { + "$ref": "#/components/schemas/ListingBuyerPrice" + } + ], + "nullable": true } } }, @@ -14581,6 +14641,75 @@ } } }, + "ListingBuyerPrice": { + "type": "object", + "x-resource-id": "ListingBuyerPrice", + "description": "The buyer-facing price for a listing, including VAT, inclusive shipping (UK), and active promotions.", + "properties": { + "base_price": { + "description": "The pre-discount listing price with VAT applied, excluding shipping. When a promotion is active, this is the price before the discount is applied.", + "oneOf": [ + { + "$ref": "#/components/schemas/Money" + } + ] + }, + "shipping_cost": { + "description": "The shipping cost with VAT applied. Only present for UK buyers.", + "oneOf": [ + { + "$ref": "#/components/schemas/Money" + } + ], + "nullable": true + }, + "original_price": { + "description": "The all-in display price (base + shipping for UK). This is the price to show to the buyer.", + "oneOf": [ + { + "$ref": "#/components/schemas/Money" + } + ] + }, + "discounted_price": { + "description": "The sale price (all-in). Null if no active promotion.", + "oneOf": [ + { + "$ref": "#/components/schemas/Money" + } + ], + "nullable": true + }, + "discount_amount": { + "description": "The discount amount as money (original_price - discounted_price). Null if no active promotion.", + "oneOf": [ + { + "$ref": "#/components/schemas/Money" + } + ], + "nullable": true + }, + "discount_percentage": { + "type": "integer", + "description": "The discount percentage (e.g. 20 for 20% off). Null if no active promotion or if the promotion is a fixed-amount discount.", + "nullable": true + }, + "has_discount": { + "type": "boolean", + "description": "Whether an active promotion applies to this listing." + }, + "discount_start_epoch": { + "type": "integer", + "description": "The start timestamp of the active promotion. Null if no active promotion.", + "nullable": true + }, + "discount_end_epoch": { + "type": "integer", + "description": "The end timestamp of the active promotion. Null if no active promotion.", + "nullable": true + } + } + }, "ListingImages": { "type": "object", "x-resource-id": "ListingImages", diff --git a/tests/test_listing_resource.py b/tests/test_listing_resource.py index c8f62aa..d363a7b 100644 --- a/tests/test_listing_resource.py +++ b/tests/test_listing_resource.py @@ -185,6 +185,30 @@ def test_is_safe_defaults_to_none(self, mock_session): qp = mock_session.make_request.call_args[1]["query_params"] assert qp["is_safe"] is None + def test_with_buyer_country_and_currency(self, mock_session): + mock_session.make_request.return_value = Response( + 200, make_shop_listing_collection() + ) + resource = ListingResource(session=mock_session) + + resource.find_all_listings_active(buyer_country="DE", currency="EUR") + + qp = mock_session.make_request.call_args[1]["query_params"] + assert qp["buyer_country"] == "DE" + assert qp["currency"] == "EUR" + + def test_buyer_country_and_currency_default_to_none(self, mock_session): + mock_session.make_request.return_value = Response( + 200, make_shop_listing_collection() + ) + resource = ListingResource(session=mock_session) + + resource.find_all_listings_active() + + qp = mock_session.make_request.call_args[1]["query_params"] + assert qp["buyer_country"] is None + assert qp["currency"] is None + class TestFindAllActiveListingsByShop: def test_basic_call(self, mock_session): @@ -234,6 +258,32 @@ def test_legacy_defaults_to_none(self, mock_session): qp = mock_session.make_request.call_args[1]["query_params"] assert qp["legacy"] is None + def test_with_buyer_country_and_currency(self, mock_session): + mock_session.make_request.return_value = Response( + 200, make_shop_listing_collection() + ) + resource = ListingResource(session=mock_session) + + resource.get_listings_by_listing_ids( + [111, 222], buyer_country="GB", currency="GBP" + ) + + qp = mock_session.make_request.call_args[1]["query_params"] + assert qp["buyer_country"] == "GB" + assert qp["currency"] == "GBP" + + def test_buyer_country_and_currency_default_to_none(self, mock_session): + mock_session.make_request.return_value = Response( + 200, make_shop_listing_collection() + ) + resource = ListingResource(session=mock_session) + + resource.get_listings_by_listing_ids([111]) + + qp = mock_session.make_request.call_args[1]["query_params"] + assert qp["buyer_country"] is None + assert qp["currency"] is None + class TestGetListingsByListingsIds: def test_listing_ids_joined(self, mock_session):